[C++] virtual析构函数

有许多种做法可以记录时间,
因此,设计一个TimeKeeper base class和一些derived class作为不同的计时方法,相当合情合理。

class TimeKeeper{
public:
    TimeKeeper();
    ~TimeKeeper();
    ...
};

// 原子钟
class AtomicClock: public TimeKeeper { ... };

// 水钟
class WaterClock: public TimeKeeper { ... };

// 腕表
class WristWatch: public TimeKeeper { ... };

许多客户只想在程序中使用时间,不想操心时间如何计算等细节,
这时候我们可以设计factory(工厂)函数,返回指针指向一个计时对象。
factory函数会“返回一个base class指针,指向新生成之derived class对象”:

// 返回一个指针,指向一个TimeKeeper派生类的动态分配对象
TimeKeeper* getTimeKeeper();

为遵守factory函数的规矩,被getTimeKeeper()返回的对象必须位于heap。
因此为了避免泄漏内存和其他资源,将factory函数返回的每一个对象,适当的delete掉很重要。

// 从TimeKeeper继承体系获得一个动态分配对象
TimeKeeper* ptk = getTimeKeeper();
...

// 释放它,避免资源泄漏
delete ptk;

1. 局部销毁

虽然倚赖客户执行delete动作,基本上便带有某种错误倾向,
factory函数接口也该修改以便预发常见之客户错误,
但这些在此都是次要的,因为纵使客户把每一件事都做对了,仍然没办法知道程序如何行动。

为题出在getTimeKeeper返回的指针指向一个derived class对象(例如AtomicClock),
而那个对象却经由一个base class指针(例如一个TimeKeeper*指针被删除),
而目前的base class(TimeKeeper)有个non-virtual析构函数。

这是一个引来灾难的秘诀,因为C++明确指出,
当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未定义。
实际执行时,通常发生的是,对象的derived成分没有被销毁。

如果getTimeKeeper返回指针指向一个AtomicClock对象,
在其内的AtomicClock成分(也就是声明于AtomicClock class内的成员变量)很可能没被销毁。
AtomicClock的析构函数也未能执行起来。
然而其base class成分(也就是TimeKeeper这一部分)通常会被销毁,于是造成一个诡异的“局部销毁”对象。
这可是形成资源泄露,败坏之数据结构,在调试器上浪费许多时间的绝佳途径喔。

消除这个问题的做法很简单,给base class一个virtual析构函数。
此后删除derived class对象就会如你想要的那般。
是的,它会销毁整个对象,包括所有的derived class成分。

TimeKeeper这样的base class除了析构函数之外,通常还有其他的virtual函数,
因为virtual函数的目的,是允许derived class的实现得以客制化。
例如,TimeKeeper就可能拥有一个virtual getCurrentTime,它在不同的derived class中有不同的实现码。
任何class只要带有virtual函数,都几乎确定应该也有一个virtual析构函数。

2. vptr指针

如果class不包含virtual函数,通常表示它并不意图用作一个base class。
当class不企图被当做base class,令其析构函数为virtual往往是个馊主意。
欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。
这份信息通常是由一个所谓vptr(virtual table pointer)指针指出。
vptr指向一个由函数指针构成的数组,称为vtbl(virtual table)。

每一个带有virtual函数的class都有一个相应的vtbl,当对象调用某一个virtual函数,
实际被调用的函数,取决于该对象的vptr所指的那个vtbl,编译器在其中寻找适当的函数指针。

virtual函数的实现细节不重要,重要的是如果class内含virtual函数,其对象的体积会增加。
vptr指针,在32-bit计算机体系结构中,将多占用32bits,在64-bit计算机体系结构中,会多占用64-bits。
因此,无端的将所有的class的析构函数声明为virtual,就像从未声明它们为virtual一样,都是错误的。
许多人的心得是,只有当class内含至少一个virtual函数,才为它声明virtual析构函数。

3. non-virtual析构函数

即使class完全不带virtual函数,被“non-virtual析构函数问题”给咬伤还是有可能的。
举个例子,标准string不含任何virtual函数,但有时候程序员会错误的把它当做base class:

// 馊主意,std::string有个non-virtual析构函数
class SpecialString: public std::string{
    ...
};

乍看似乎无害,但如果你在程序任意某处无意间将一个pointer to SpecialString转换成一个pointer to string
然后将转换所得的那个string指针delete掉,你立刻被流放到“行为不明确”的恶地上。

SpecialString* pss = new SpecialString("Impending Doom");
std::string* ps;
...

// SpecialString* => std::string*
ps = pss;
...

// 未有定义,现实中*ps的SpecialString资源会泄露,
// 因为SpecialString的析构函数没被调用
delete ps;

相同的分析适用于任何不带virtual析构函数的class,包括所有STL容器,如vectorlistsettr1::unordered_map等等。
如果你曾经企图继承一个标准容器或任何其他“带有non-virtual析构函数”的class,拒绝诱惑吧。
(很不幸C++没有提供类似Java的final class或C#的sealed class那样的“禁止派生”机制)

4. pure virtual析构函数

有时候,令class带一个pure virtual析构函数,可能颇为便利。
pure virtual函数,导致abstract(抽象) class,也就是不能被实体化(instantiated)的class。
也就是说,你不能为那种类型创建对象。

class AMOV{
public:

    // 声明为pure virtual析构函数
    virtual ~AMOV() = 0;
};

这个class有一个pure virtual函数,所以它是个抽象class,
又由于它有个virtual析构函数,所以你不需要担心析构函数的问题。

析构函数的运作方式是,最深层(most derived)的那个class其析构函数最先被调用,
然后是其每一个base class的析构函数被调用。
编译器会在AMOV的derived class的析构函数中创建一个对~AMOV的调用动作,
所以,你必须为这个函数提供一份定义,如果不这样做,连接器会发出抱怨。

// pure virtual析构函数的定义
AMOV::~AMOV() { }

总结

“给base class一个virtual析构函数”,这个规则只适用于polymorphic(带多态性质)的base class身上。
这种base class的设计目的是为了用来“通过base class接口处理derived class对象”。
TimeKeeper就是一个polymorphic base class,
因为我们希望处理AtomicClockWaterClock对象,纵使我们只有TimeKeeper指针指向它们。

并非所有的base class的设计目的都是为了多态用途,
例如标准string和STL容器,都不被设计作为base class使用,更别提多态了。
某些class的设计目的是作为base class使用,但不是为了多态用途,
它们并非设计用来“经由base class接口处置derived class对象”,因此,它们不需要virtual析构函数。


Effective C++ - P40

你可能感兴趣的:([C++] virtual析构函数)