条款07: 为多态基类声明 virtual 析构函数
Declare destructors virtual in polymorphic base classes
设计以下时间基类TimeKeeper:
class TimeKeeper { public: TimeKeeper(); ~TimeKeeper(); }; class AtomicClock : public TimeKeeper { ... }; class WaterClock : public TimeKeeper { ... }; class WristWatch : public TimeKeeper { ... };
许多客户只想在程序中使用时间,不想操心时间如何计算等细节,这是可以设计factory(工厂)函数,返回指针指向一个计时对象.Factory函数会"返回一个base class指针,指向新生成的derived class对象":
TimeKeeper *getTimeKeeper(); // 返回一个指针,指向TimeKeeper派生类的动态分配对象
为遵守factory函数的规矩,将getTimeKeeper()返回的对象必须位于heap.因此为了避免泄露内存和其他资源,将factory函数返回的每一个对象适当地 delete 掉很重要:
TimeKeeper *ptk = getTimeKeeper(); // 从TimeKeeper继承体系获得一个动态分配对象 ... // 运用它 delete ptk; // 释放它,避免资源泄露
上述代码的饿一个弱点是:纵使客户把每一件事都做对了,仍然没办法知道程序如何行动.
问题出在getTimeKeeper返回的指针指向一个derived class 对象(例如AutomicClock),而那个对象却经由一个base class 指针(例如一个TimeKeeper*指针)被删除,而目前的base class(Timekeeper)有个non-virtual 析构函数.
引来灾难的原因是,C++明确指出,当derived class 对象经由一个base class 指针来删除,而该base class 带着一个non-virtual 析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没有被销毁.如果getTimeKeeper返回指针指向一个AtomicClock对象,其内的AtomicClock成分很可能没被销毁,而AtomicClock的析构函数也未能执行起来.然而其base class 成分通常会被销毁,于是造成一个"局部销毁"对象,这可能形成资源泄露,败坏数据结构.
消除这个问题的办法很简单:给base class 一个 virtual 析构函数,此后删除derived class 对象就会达到目的.它会销毁整个对象,包括所有derived class 成分:
class TimeKeeper { public: TimeKeeper(); virtual ~TimeKeeper(); }; TimeKeeper *ptk = getTimeKeeper(); ... delete ptk; // 现在行为正确
像TimeKeeper这样的base classes除了析构函数之外通常还有其他 virtual 函数,因为 virtual 函数的目的是允许derived class 的实现得以定制化.例如TimeKeeper就可能拥有一个 virtual getCurrentTime,它在不同的derived class 中有不同的实现码.任何 class 只要带有 virtual 函数都几乎确定应该也有一个 virtual 析构函数.
如果 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 函数实现细节不重要,重要的如果一个类内含 virtual 函数,其对象的体积会增加.C++的对象也不再和其他语言(如C)内的相同声明有着一样的结构,因此也就不能把它传递给其他语言所写的函数.
因此,无端地将所有 class 的析构函数声明为 virtual,是错误的.通常的做法是:只有当 class 内含至少一个 virtual 函数,才为它声明 virtual 析构函数.
即使 class 完全不带 virtual 函数,被"non-virtual析构函数问题"给伤到还是有可能的.例如,标准string不含任何 virtual 函数,但有时程序员会错误地把它当做base class:
class SpecialString : public std::string { };
如果在程序任意某处无意间将一个pointer-to-SpecialString转换为一个pointer-to-string,然后将转换所得到的那个string指针 delete 掉,则会出现之前的泄露问题.
相同的分析适用于任何不带 virtual 析构函数的 class,包括所有STL容器如vector,list,set等等.因此要拒绝继承一个标准容器或任何其他"带有non-virtual析构函数"的 class.
有时候令 class 带一个pure virtual 析构函数,可能颇为便利.pure virtual 函数导致抽象abstract class——即不能被实体化(instantiated)的 class.也就是说不能为那种类型创建对象.然而有时候希望拥有抽象 class,但没有任何pure virtual 函数,怎么办?由于抽象 class 总是被当作一个 base class 来用,而由于base class 应该有个 virtual 析构函数,并且由于pure virtual 函数会导致抽象 class,因此办法很简单:为希望它成为抽象的那个
class 声明一个 pure virtual 析构函数.例如:
class AWOV { public: virtual ~AWOV() = 0; // 声明pure virtual析构函数 };
这个 class 有一个pure virtual 函数,所以它是个抽象 class,又由于它有个 virtual 析构函数,所以无需担心析构函数的问题.这里有一个窍门:必须为这个pure virtual 析构函数提供一份定义:
AWOV::~AWOV() {} // pure virtual 析构函数的定义
析构函数的运作方式是:最深层派生(most derived)的那个 class 其析构函数最先被调用,然后是其每一个base class 的析构函数被调用.编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用操作,所以必须为这个函数提供一份定义.
定义纯虚析构函数的必要性:派生类的析构函数被调用,然后调用其每一个base class 的析构函数,最终抽象类的纯虚析构函数一定会被调用.如果纯虚析构函数只是声明,没有定义,那么就会造成运行时崩溃.而纯虚析构函数的空实现(如上面那行所示)能够保证这样的代码的安全性.
此外还可以利用纯虚成员函数定位错误代码(详见定义纯虚析构函数).
"给base classes一个virtual析构函数",这个规则只适用于polymorphic base classes上.这种base classes的设计目的是为了用来"通过base class接口处理derived class对象".TimeKeeper就是一个polymorphic base class,因为希望处理它的不同的派生类对象.
并非所有base classes的设计目的都是为了多态用途.例如标准string和STL容器都不被设计作为base classes使用,更不用提多态了.某些classes的设计目的是作为base classes使用,但不是为了多态用途,因此它们不需要virtual析构函数.
注意:
polymorphic base classes应该声明一个 virtual 析构函数.如果 class 带有任何 virtual 函数,它就应该拥有一个 virtual 析构函数.
classes的设计目的如果不是作为base classes使用,或不是为了具备多态性,就不应该声明 virtual 析构函数.
版权声明:本文为博主原创文章,未经博主允许不得转载。