条款07:为多态基类声明virtual析构函数
Declare destructors virtual in polymorphic base classes
接着上一篇的学习。
不要无端将析构函数声明为virtual
如果class不含virtual函数们通常表示它并不准备作为一个base class。而当一个class并不准备作为base class时,令其析构函数为virtual往往会出现问题。举个例子,定义一个表示二维坐标系下点的class:
class Point {
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};
对于上面的代码,如果int所占用的字节是32bits,那么这个Point对象是可以放入一个64-bit的缓存器猴子那个的。甚至,这样的一个Point对象可以被作为一个“64-bit量”传给以其他语言(C语言或者FORTRAN)所撰写的函数。
然而,如果将Point的析构函数写为virtual,情况就有所变化:
首先,如果我们想要实现一个virtual函数,对象就必须携带某些信息。
某些信息——用来在运行期间来决定哪一个virtual函数应该被调用。
这份信息则通常是由一个vptr(virtual table pointer)指针所指出。
- vptr指向一个由函数指针构成的数组——称为vtbl(virtual table)。
- 每一个带有virtual函数的class都有一个对应的vtbl。
- 当对象调用某一个virtual函数,实际被调用的函数取决于该对象的vptr所指的那个vtbl——编译器则会在其中寻找适当的函数指针。
因此,当我们如果将析构函数声明为virtual,整个对象的体积会增加:在32-bit计算机体系结构中将会占用64 bits(为了存放两个int),或者96 bits(两个int加上vptr);而在64-bit计算机体系结构中则可能会占用64~128bits,因为指针在这样的计算机体系结构占用64 bits。
因此,为Point添加一个vptr会将其对象的大小增加50%~100%!这样,Point对象也不可以和其他语言(例如C语言)内的相同声明有这一样的结构(因为其他语言的对应并没有vptr),因此,也就不再可能把它传递给(或接受自)其他语言所编写的函数,因此,也就丧失了移植性。
因此:
- 无端地将所有classes的析构函数声明为virtual,就像从未声明它们为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掉,就会立刻出现“行为不明确”的问题:
SpecialString* pss = new SpecialString("Impending Doom!");
std::string* ps;
...
ps = pss; //SpecialString* ---> std::stirng*
...
delete ps; //没有定义!因为现实中的*ps的SpecialString相关资源会发生泄漏
//因为SpecialString析构函数没有被调用
相同的分析过程,适用于任何不带virtual析构函数的class,包括所有的STL容器(vector、list、set、unordered_map等等)因此,
- 不要继承任何标准容器或者“带有non-virtual析构函数”的class!
pure virtual 析构函数
有的时候,令class带一个pure virtual析构函数,可能会比较方便。
首先,带有pure virtual函数的class为abstract(抽象)class——即不能被实例化的class,换句话说,我们不能为这种类创建对象。
由于抽象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 classes定义一个virtual析构函数”这条规则,只适用于polymorphic(带有多态性质的)base class身上。这种base classes的设计目的就是为了用来“通过base class接口处理derived class对象”。TimeKeeper就是一个polymorphic base class,因为我们希望它可以处理多个不同的对象,即使我们只有TimeKeeper指针去指向它们。
但是,并不是所有的base class设计的目的都是为了多态。例如标准string和STL容器都不被设计作为base classes使用。另外,某些classes的设计目的是作为base class来使用,但是并不是为了多态用途。
最后: