# Item 7: 在 polymorphic base classes(多态基类)中将 destructors(析构函数)声明为 virtual(虚拟)
作者:Scott Meyers
译者:fatalerror99 (iTePub's Nirvana)
发布:http://blog.csdn.net/fatalerror99/
有很多方法取得时间,所以有必要建立一个 TimeKeeper base class(基类),并为不同的计时方法建立 derived classes(派生类):
```
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper { ... };
class WaterClock: public TimeKeeper { ... };
class WristWatch: public TimeKeeper { ... };
```
很多客户只是想简单地取得时间而不关心如何计算的细节,所以一个 factory function(工厂函数)——返回 a base class pointer to a newly-created derived class object(一个指向新建派生类对象的基类指针)的函数——可以被用来返回一个指向 timekeeping object(计时对象)的指针:
```
TimeKeeper* getTimeKeeper(); // returns a pointer to a dynamic-
// ally allocated object of a class
// derived from TimeKeeper
```
与 factory function(工厂函数)的惯例一致,getTimeKeeper 返回的 objects(对象)是建立在 heap(堆)上的,所以为了避免泄漏内存和其它资源,每一个返回的 objects(对象)被完全 deleted 是很重要的。
```
TimeKeeper *ptk = getTimeKeeper(); // get dynamically allocated object
// from TimeKeeper hierarchy
... // use it
delete ptk; // release it to avoid resource leak
```
Item 13 解释了为什么依赖客户执行删除任务是 error-prone(错误倾向),Item 18 解释了 factory function(工厂函数)的 interface(接口)应该如何改变以防止普通的客户错误,但这些在这里都是次要的,因为在这个 Item 中,我们将精力集中于上面的代码中一个更基本的缺陷:即使客户做对了每一件事,也无法预知程序将如何运转。
问题在于 getTimeKeeper 返回一个 pointer to a derived class object(指向派生类对象的指针)(比如 AtomicClock),那个 object(对象)经由一个 base class pointer(基类指针)(也就是一个 TimeKeeper\* pointer)被删除,而且这个 base class(基类)(TimeKeeper) 有一个 non-virtual destructor(非虚拟析构函数)。祸端就在这里,因为 C++ 规定:当一个 derived class object(派生类对象)通过使用一个 pointer to a base class with a non-virtual destructor(指向带有非虚拟析构函数的基类的指针)被删除,则结果是未定义的。运行时比较典型的后果是 derived part of the object(这个对象的派生部分)不会被析构。如果 getTimeKeeper 返回一个指向 AtomicClock object(对象)的指针,则 object(对象)的 AtomicClock 部分(也就是在 AtomicClock class 中声明的 data members(数据成员))很可能不会被析构,AtomicClock 的 destructor(析构函数)也不会运行。然而,base class part(基类部分)(也就是 TimeKeeper 部分)很可能已被析构,这就导致了一个古怪的 "partially destroyed" object(“部分被析构”对象)。这是一个导致泄漏资源,破坏数据结构以及消耗大量调试时间的绝妙方法。
消除这个问题很简单:给 base class(基类)一个 virtual destructor(虚拟析构函数)。于是,删除一个 derived class object(派生类对象)的时候就有了你所期望的行为。将析构 entire object(整个对象),包括全部的 derived class parts(派生类构件):
```
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
TimeKeeper *ptk = getTimeKeeper();
...
delete ptk; // now behaves correctly
```
类似 TimeKeeper 的 base classes(基类)一般都包含除了 destructor(析构函数)以外的其它 virtual functions(虚拟函数),因为 virtual functions(虚拟函数)的目的就是允许 derived class implementations(派生类实现)的定制化(参见 Item 34)。例如,TimeKeeper 可以有一个 virtual functions(虚拟函数)getCurrentTime,它在各种不同的 derived classes(派生类)中有不同的实现。几乎所有拥有 virtual functions(虚拟函数)的 class(类)差不多都应该有一个 virtual destructor(虚拟析构函数)。
如果一个 class(类)不包含 virtual functions(虚拟函数),这经常预示不打算将它作为 base class(基类)使用。当一个 class(类)不打算作为 base class(基类)时,将 destructor(析构函数)虚拟通常是个坏主意。考虑一个表现二维空间中的点的 class(类):
```
class Point { // a 2D point
public:
Point(int xCoord, int yCoord);
~Point();
private:
int x, y;
};
```
如果一个 int 占用 32 bits,一个 Point object 正好适用于 64-bit 的寄存器。而且,这样一个 Point object 可以被作为一个 64-bit 的量传递给其它语言写的函数,比如 C 或者 FORTRAN。如果 Point 的 destructor(析构函数)被虚拟,情况就完全不一样了。
virtual functions(虚拟函数)的实现要求 object(对象)携带额外的信息,这些信息用于在运行时确定该 object(对象)应该调用哪一个 virtual functions(虚拟函数)。典型情况下,这一信息具有一种被称为 vptr ("virtual table pointer")(虚拟函数表指针)的指针的形式。vptr 指向一个被称为 vtbl ("virtual table")(虚拟函数表)的 array of function pointers(函数指针数组),每一个带有 virtual functions(虚拟函数)的 class(类)都有一个相关联的 vtbl。当在一个 object(对象)上调用 virtual functions(虚拟函数)时,实际的被调用函数通过下面的步骤确定:找到 object(对象)的 vptr 指向的 vtbl,然后在 vtbl 中寻找合适的 function pointer(函数指针)。
virtual functions(虚拟函数)是如何实现的细节并不重要。重要的是如果 Point class 包含一个 virtual functions(虚拟函数),这个类型的 object(对象)的大小就会增加。在一个 32-bit 架构中,它们将从 64 bits(相当于两个 ints)长到 96 bits(两个 ints 加上 vptr);在一个 64-bit 架构中,它们可能从 64 bits 长到 128 bits,因为在这样的架构中指针的大小是 64 bits 的。为 Point 加上 vptr 将会使它的大小增长 50-100%!Point object(对象)不再适合 64-bit 寄存器。而且,Point object(对象)在 C++ 和其它语言(比如 C)中,看起来不再具有相同的结构,因为它们在其它语言中的对应物没有 vptr。结果,Points 不再可能传入其它语言写成的函数或从其中传出,除非你为 vptr 做出明确的补偿,而这是它自己的实现细节并因此失去可移植性。
最终结果就是无故地将所有 destructors(析构函数)声明为 virtual(虚拟),和从不把它们声明为 virtual(虚拟)一样是错误的。实际上,很多人总结过这条规则:declare a virtual destructor in a class if and only if that class contains at least one virtual function(当且仅当一个类中包含至少一个虚拟函数时,则在类中声明一个虚拟析构函数)。
甚至在完全没有 virtual functions(虚拟函数)时,也有可能纠缠于 non-virtual destructor(非虚拟析构函数)问题。例如,标准 string 类型不包含 virtual functions(虚拟函数),但是被误导的程序员有时将它当作 base class(基类)使用:
```
class SpecialString: public std::string { // bad idea! std::string has a
... // non-virtual destructor
};
```
一眼看上去,这可能无伤大雅,但是,如果在程序的某个地方因为某种原因,你将一个 pointer-to-SpecialString(指向 SpecialString 的指针)转型为一个 pointer-to-string(指向 string 的指针),然后你将 delete 施加于这个 string pointer(指针),你就立刻被放逐到 undefined behavior(未定义行为)的领地:
```
SpecialString *pss = new SpecialString("Impending Doom");
std::string *ps;
...
ps = pss; // SpecialString* → std::string*
...
delete ps; // undefined! In practice,
// *ps's SpecialString resources
// will be leaked, because the
// SpecialString destructor won't
// be called.
```
同样的分析可以适用于任何缺少 virtual destructor(虚拟析构函数)的 class(类),包括全部的 STL container(容器)类型(例如,vector,list,set,tr1::unordered_map(参见 Item 54)等)。如果你受到从 standard container(标准容器)或任何其它带有 non-virtual destructor(非虚拟析构函数)的 class(类)继承的诱惑,一定要挺住!(不幸的是,C++ 不提供类似 Java 的 final classes(类)或 C# 的 sealed classes(类)的 derivation-prevention mechanism(防派生机制)。)
有时候,给一个 class(类)提供一个 pure virtual destructor(纯虚拟析构函数)能提供一些便利。回想一下,pure virtual functions(纯虚拟函数)导致 abstract classes(抽象类)——不能被实例化的 classes(类)(也就是说你不能创建这个类型的 objects(对象))。然而,有时候,你有一个 class(类),你希望它是抽象的,但没有任何 pure virtual functions(纯虚拟函数)。怎么办呢?因为一个 abstract classes(抽象类)注定要被用作 base class(基类),又因为一个 base class(基类)应该有一个 virtual destructor(虚拟析构函数),还因为一个 pure virtual functions(纯虚拟函数)产生一个 abstract classes(抽象类),好了,解决方案很简单:在你想要变成抽象的 class(类)中声明一个 pure virtual destructor(纯虚拟析构函数)。这是一个例子:
```
class AWOV { // AWOV = "Abstract w/o Virtuals"
public:
virtual ~AWOV() = 0; // declare pure virtual destructor
};
```
这个 class(类)有一个 pure virtual functions(纯虚拟函数),所以它是抽象的,又因为它有一个 virtual destructor(虚拟析构函数),所以你不必担心析构函数问题。这是一个螺旋。然而,你必须为 pure virtual destructor(纯虚拟析构函数)提供一个 definition(定义):
```
AWOV::~AWOV() {} // definition of pure virtual dtor
```
destructors(析构函数)的工作方式是:most derived class(层次最低的派生类)的 destructor(析构函数)最先被调用,然后调用每一个 base class(基类)的 destructors(析构函数)。编译器会生成一个从它的 derived classes(派生类)的 destructors(析构函数)对 ~AWOV 的调用,所以你不得不确保为函数提供一个函数体。如果你不这样做,连接程序会提出抗议。
为 base classes(基类)提供 virtual destructor(虚拟析构函数)的规则仅仅适用于 polymorphic base classes(多态基类)—— base classes(基类)被设计成允许通过 base class interfaces(基类接口)对 derived class types(派生类类型)进行操作。TimeKeeper 就是一个 polymorphic base classes(多态基类),因为即使我们只有类型为 TimeKeeper 的 pointers(指针)指向它们的时候,我们也期望能够操作 AtomicClock 和 WaterClock objects(对象)。
并非所有的 base classes(基类)都被设计用于 polymorphically(多态)。例如,无论是 standard string type(标准 string 类型),还是 STL container types(STL 容器类型)全被设计成 base classes(基类),可没有哪个是 polymorphic(多态)的。一些 classes(类)虽然被设计用于 base classes(基类),但并非被设计用于 polymorphically(多态)。这样的 classes(类)——例如 Item 6 中的 Uncopyable 和标准库中的 input_iterator_tag(参见 Item 47)——没有被设计成允许经由 base class interfaces(基类接口)对 derived class objects(派生类对象)进行操作。所以它们就不需要 virtual destructor(虚拟析构函数)。
Things to Remember
* polymorphic base classes(多态基类)应该声明 virtual destructor(虚拟析构函数)。如果一个 class(类)有任何 virtual functions(虚拟函数),它就应该有一个 virtual destructor(虚拟析构函数)。
* 不是设计用来作为 base classes(基类)或不是设计用于 polymorphically(多态)的 classes(类)就不应该声明 virtual destructor(虚拟析构函数)。
- Preface(前言)
- Introduction(导言)
- Terminology(术语)
- Item 1: 将 C++ 视为 federation of languages(语言联合体)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 确保 objects(对象)在使用前被初始化
- Item 5: 了解 C++ 为你偷偷地加上和调用了什么函数
- Item 6: 如果你不想使用 compiler-generated functions(编译器生成函数),就明确拒绝
- Item 7: 在 polymorphic base classes(多态基类)中将 destructors(析构函数)声明为 virtual(虚拟)
- Item 8: 防止因为 exceptions(异常)而离开 destructors(析构函数)
- Item 9: 绝不要在 construction(构造)或 destruction(析构)期间调用 virtual functions(虚拟函数)
- Item 10: 让 assignment operators(赋值运算符)返回一个 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中处理 assignment to self(自赋值)
- Item 12: 拷贝一个对象的所有组成部分
- Item 13: 使用对象管理资源
- Item 14: 谨慎考虑资源管理类的拷贝行为
- Item 15: 在资源管理类中准备访问裸资源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一个独立的语句中将 new 出来的对象存入智能指针
- Item 18: 使接口易于正确使用,而难以错误使用
- Item 19: 视类设计为类型设计
- Item 20: 用 pass-by-reference-to-const(传引用给 const)取代 pass-by-value(传值)
- Item 21: 当你必须返回一个对象时不要试图返回一个引用
- Item 22: 将数据成员声明为 private
- Item 23: 用非成员非友元函数取代成员函数
- Item 24: 当类型转换应该用于所有参数时,声明为非成员函数
- Item 25: 考虑支持不抛异常的 swap
- Item 26: 只要有可能就推迟变量定义
- Item 27: 将强制转型减到最少
- Item 28: 避免返回对象内部构件的“句柄”
- Item 29: 争取异常安全(exception-safe)的代码
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之间的编译依赖
- Item 32: 确保 public inheritance 模拟 "is-a"
- Item 33: 避免覆盖(hiding)“通过继承得到的名字”
- Item 34: 区分 inheritance of interface(接口继承)和 inheritance of implementation(实现继承)
- Item 35: 考虑可选的 virtual functions(虚拟函数)的替代方法
- Item 36: 绝不要重定义一个 inherited non-virtual function(通过继承得到的非虚拟函数)
- Item 37: 绝不要重定义一个函数的 inherited default parameter value(通过继承得到的缺省参数值)
- Item 38: 通过 composition(复合)模拟 "has-a"(有一个)或 "is-implemented-in-terms-of"(是根据……实现的)
- Item 39: 谨慎使用 private inheritance(私有继承)
- Item 40: 谨慎使用 multiple inheritance(多继承)
- Item 41: 理解 implicit interfaces(隐式接口)和 compile-time polymorphism(编译期多态)
- Item 42: 理解 typename 的两个含义
- Item 43: 了解如何访问 templatized base classes(模板化基类)中的名字
- Item 44: 从 templates(模板)中分离出 parameter-independent(参数无关)的代码
- Item 45: 用 member function templates(成员函数模板) 接受 "all compatible types"(“所有兼容类型”)
- Item 46: 需要 type conversions(类型转换)时在 templates(模板)内定义 non-member functions(非成员函数)
- Item 47: 为类型信息使用 traits classes(特征类)
- Item 48: 感受 template metaprogramming(模板元编程)
- Item 49: 了解 new-handler 的行为
- Item 50: 领会何时替换 new 和 delete 才有意义
- Item 51: 编写 new 和 delete 时要遵守惯例
- Item 52: 如果编写了 placement new,就要编写 placement delete
- 附录 A. 超越 Effective C++
- 附录 B. 第二和第三版之间的 Item 映射