有许多种做法可以记录时间,
因此,设计一个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析构函数。