1️⃣ 问题的引入

  • 有许多种做法可以记录时间,因此,设计一个TimeKeeper base class和一些
    derived classes作为不同的计时方法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class TimeKeeper
    {
    public:
    TimeKeeper () ;
    ~TimeKeeper () :
    ...
    };

    class AtomicClock: public TimeKeeper { ... }; //原子钟
    class WaterClock: public TimeKeeper { ... }; //水钟
    class WristWatch: public TimeKeeper { ... }; //腕表
  • 许多客户只想在程序中使用时间,不想操心时间如何计算等细节,这应该怎么办呢 ?
    • 使用factory函数
      • 返回指针指向一个计时对象。Factory函数会 ”返回一个base class指针,指向新生成之derived class对象” :
        1
        2
        TimeKeeper* getTimeKeeper();    //返回一个指针,指向一个
        //TimeKeeper派生类的动态分配对象

2️⃣ 这种方式的缺点

  • 根本问题
    • 问题出在getTimeKeeper返回的指针指向一个derived class对象(例如AtomicClock),而那个对象却经由一个base class指针(例如一个TimeKeeper*指针)被删除,而目前的base classTimeKeeper)有个non-virtual析构函数。
  • 引发的灾难
    • derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义 :
      • 实际执行时通常发生的是对象的derived成分没被销毁。如果getTimeKeeper返回指针指向一个AtomicClock对象,其内的AtomicClock成分(也就是声明于AtomicClock class内的成员变量)很可能没被销毁,而AtomicClock的析构函数也未能执行起来。然而其base class成分(也就是TimeKeeper这一部分)通常会被销毁,于是造成一个诡异的“局部销毁”对象。

3️⃣ 解决方法

  • base class一个virtual析构函数。
  • 此后删除derived class对象就会销毁整个对象,包括所有derived class成分
    1
    2
    3
    4
    5
    6
    7
    8
    class TimeKeeper{
    public:
    TimeKeeper();
    virtual ~TimeKeeper () :
    };

    TimeKeeper* ptk = getTimeKeeper ();
    delete ptk; //现在,行为正确。

4️⃣ 析构函数声明为虚函数的时机

  • 定义
    • 如果class不含virtual函数,通常表示它并不意图被用做一个base class
    • class不企图被当作base class,令其析构函数为virtual往往是个馊主意。
  • 案例引入
    1
    2
    3
    4
    5
    6
    7
    8
    class Point 
    {
    public:
    Point(int xCoord, int yCoord) ;
    ~Point () ;
    private:
    int x, y;
    };
    如果int占用 32 bits,那么Point对象可塞入一个64-bit 缓存器中。然而当Point的析构函数是virtual,形势起了变化。
  • 🤓虚函数原理
    • 欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。
    • 这份信息通常是由一个所谓vptrvirtual table pointer)指针指出。
    • vptr指向一个由函数指针构成的数组,称为vtbl(virtual table);每一个带有virtual函数的class都有一个相应的vtbl。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl——编译器在其中寻找适当的函数指针。
  • 🥵额外的开销
    • 如果Point class内含virtual函数,其对象的体积会增加:
      • 在 32-bit 计算机体系结构中将占用64bits(存放两个ints)至96 bits(两个ints加上vptr);
      • 在 64-bit 计算机体系结构中可能占用64128 bits,因为指针在这样的计算机结构中占 64bits 。因此,为Point添加一个vptr会增加其对象大小达 50%100% !