有许多种做法可以记录时间,
因此,设计一个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容器,如vector,list,set,tr1::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,
因为我们希望处理AtomicClock和WaterClock对象,纵使我们只有TimeKeeper指针指向它们。
并非所有的base class的设计目的都是为了多态用途,
例如标准string和STL容器,都不被设计作为base class使用,更别提多态了。
某些class的设计目的是作为base class使用,但不是为了多态用途,
它们并非设计用来“经由base class接口处置derived class对象”,因此,它们不需要virtual析构函数。