Effective C++读书笔记(5)

来源:岁月联盟 编辑:exp 时间:2012-02-04

条款07:为多态基类声明virtual析构函数

Declare destructors virtual inpolymorphic base classes

建立一个 TimeKeeper基类,并为不同的计时方法建立派生类:

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

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

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

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

TimeKeeper* getTimeKeeper();

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

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

...                                  //运用它

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

很多客户只是想简单地取得时间而不关心如何计算的细节,所以一个 factoryfunction(工厂函数)——返回一个指向新建派生类对象的基类指针的函数——可以被用来返回一个指向计时对象的指针。与工厂函数的惯例一致,getTimeKeeper 返回的对象建立在堆上的,所以为了避免泄漏内存和其它资源,每一个返回的对象被适当delete掉是很重要的。

C++ 规定:当一个派生类对象通过使用一个指向non-virtual析构函数的基类的指针被删除时,则这个对象的派生部分没被销毁。如果 getTimeKeeper 返回一个指向 AtomicClock对象的指针,则对象的 AtomicClock 部分(也就是在 AtomicClock class中声明的数据成员)很可能不会被析构,AtomicClock 的析构函数也不会运行。然而,基类部分(也就是 TimeKeeper 部分)很可能已被析构,这就导致了一个诡异的局部销毁对象,导致泄漏资源。

消除这个问题很简单:给基类一个 virtual析构函数。于是,删除一个派生类对象的时候就将析构整个对象,包括所以的派生类成分。

类似 TimeKeeper 的基类一般都包含除了析构函数以外的其它virtual函数,因为virtual函数的目的就是允许派生类实现的定制化。例如,TimeKeeper 可以有一个virtual函数getCurrentTime,它在各种不同的派生类中有不同的实现。任何类只要带有virtual函数都几乎确定应该也有一个virtual析构函数。

 

如果一个类不包含virtual函数,这通常预示不打算将它作为基类使用。当一个类不打算作为基类时,令其析构函数为virtual通常是个坏主意。考虑一个表现二维空间中的点类:

class Point { // a 2D point
public:
Point(int xCoord, int yCoord);
~Point();

private:
int x, y;
};

如果一个 int 占用 32 bits,一个 Point 对象 正好适用于 64-bit 缓存器。而且,这样一个 Point 对象 可以被作为一个 64-bit 量传递给其它语言写的函数,比如 C 或者FORTRAN。而当Point 的析构函数为virtual时,要表现出virtual函数,对象必须携带额外的信息,用于在运行时确定该对象应该调用哪一个virtual虚拟函数。这一信息通常由被称为 vptr ("virtual table pointer")的指针指出,vptr 指向一个被称为 vtbl("virtual table")的函数指针数组;每一个带有 virtual函数的类都有一个相关联的 vtbl。当在一个对象上调用 virtual函数时,实际的被调用函数通过下面的步骤确定:找到对象vptr 指向的 vtbl,然后在 vtbl 中寻找合适的函数指针。

如果 Point类 包含一个 virtual函数,会为 Point 加上 vptr,将会使对象大小增长 50-100%! Point对象不再适合64-bit 寄存器。而且,Point对象在 C++ 和其它语言(比如 C)中不再具有相同的结构,因为其它语言中的对应物没有 vptr。结果,Points 不再可能传入其它语言写成的函数或从其中传出,并失去可移植性。

无故地将所有析构函数声明为 virtual,和从不把它们声明为 virtual一样是错误的。实际上,很多人总结过这条规则:当且仅当一个类中包含至少一个虚拟函数时,则在类中声明一个虚拟析构函数。

·    多态基类应该声明virtual析构函数。如果一个类带有任何 virtual函数,它就应该有一个virtual析构函数。

 

即使完全没有virtual函数,也有可能纠缠于 non-virtual析构函数问题。例如,标准 string 类型不包含 virtual函数,但是程序员有时将它当作基类使用:

class SpecialString: public std::string {
... //bad idea!std::string有个non-virtual析构函数
};

如果在程序中将一个指向 SpecialString 的指针转型为一个指向 string 的指针,然后delete 那个string指针,将导致内存泄漏,行为不明确:

SpecialString *pss = newSpecialString("Impending Doom");

std::string *ps;
...
ps = pss; // SpecialString* => std::string*
...
delete ps; /*未有定义!现实中*ps的SpecialString资源会泄漏,因为SpecialString析构函数未被调用。*/

不要企图继承标准容器(例如,vector,list,set,tr1::unordered_map)或任何其他“带有non-virtual析构函数”的类。C++ 不提供类似 Java 的 final classes或 C# 的 sealed classes那样的禁止派生机制。

有时候,给一个类提供一个 pure virtual析构函数能提供一些便利。pure virtualfunctions函数导致抽象类,也就是说你不能创建这个类型的对象。然而有时候你希望类是抽象的,但没有任何 pure virtual函数。怎么办呢?

解决方案很简单:在你想要变成抽象的类中声明一个 pure virtual析构函数:

class AWOV { // AWOV = "Abstract w/oVirtuals"
public:
virtual ~AWOV() = 0; // declare pure virtual destructor
};

这个类有一个 purevirtual函数,所以它是抽象的,又因为它有一个 virtual析构函数,所以你不必担心析构函数问题。然而,你必须为 purevirtual析构函数提供一个定义:

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

析构函数的工作方式是:最深层派生的那个类其析构函数最先被调用,然后调用其每一个基类)的析构函数。编译器会生成一个从其派生类的析构函数对 ~AWOV 的调用动作,所以你不得不为这个函数提供一份定义,不然连接器会发出抱怨。

为基类提供virtual析构函数的规则仅仅适用于 polymorphic(带多态性质的)基类上。这种基类的设计目的就是为了用来“通过基类接口处理派生类对象”。TimeKeeper 就是一个多态基类,因为即使我们只有类型为 TimeKeeper 的指针指向它们时,也期望能够操作 AtomicClock 和 WaterClock对象。

并非所有的基类的设计目的都是为了多态用途。例如,无论是标准 string还是 STL容器都不被设计成基类使用,更别提多态了。某些类虽然被设计用于基类,但并非用于多态用途。如Uncopyable 和标准库中的 input_iterator_tag,它们并非被设计用来“经由基类接口处理派生类对象”,因此不需要virtual析构函数。

·    不是设计用来作为基类或为了具备多态性的类,就不应该声明 virtual析构函数

 摘自 pandawuwyj的专栏