「生活可以更简单, 欢迎来到我的开源世界」
  1. 多态基类应该声明一个 virtual 析构函数
  2. 总结
Item7-为多态基类声明 virtual 析构函数
2021-01-01
」 「

Item7-为多态基类声明 virtual 析构函数

多态基类应该声明一个 virtual 析构函数

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

class AtomicClock: public TimeKeeper { ... }; //原子钟
class WaterClock: public TimeKeeper { ... }; //水钟
class WristWatch: public TimeKeeper { ... }; //腕表

//@ 使用时
TimeKeeper *ptk = getTimeKeeper();
...
delete ptk;

getTimeKeeper() 是一个 factory 函数,返回指针指向一个 TimeKeeper 派生类的动态分配对象,返回的对象位于 heap,因此需要对其返回的对象适当地 delete 掉。

C++ 明确指出:当派生类对象经由一个基类指针删除,而该基类指针带有 non-virtual 析构函数,则结果未定义——实际执行时通常发生的是对象的 derived 部分没被销毁,而 base 类部分通常会被销毁,造成一个诡异的“局部销毁”对象,可能造成资源泄漏、败坏数据结构、徒增调试时间。

消除这个问题的做法是:给 base 类一个 virtual 析构函数。这样通过基类指针销毁派生类将会符合期望:

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

TimeKeeper *ptk = getTimeKeeper();
...
delete ptk;

类似 TimeKeeper 这样的基类一般都包含除了析构函数以外的其它 virtual 函数,因为 virtual 函数的目的就是允许派生类的实现得以定制化。例如,TimeKeeper 可以有一个虚函数 getCurrentTime,它在各种不同的派生类中有不同的实现。几乎所有拥有虚函数的类差不多都应该有一个虚析构函数。

如果一个类不包含虚函数,通常表示它并不意图被用作一个基类。当一个类不打算作为基类时,令其析构函数为 virtual 通常是个馊主意。考虑一个用来表示二维空间中的点的类:

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 的析构函数被虚拟,情况就完全不一样了。

虚函数的实现要求对象携带额外的信息,这些信息用于在运行时确定该对象应该调用哪一个虚函数。这份信息通常由 vptr(virtual table pointer)指针指出,vptr指向一个由函数指针构成的数组,称为 vtbl(virtual table)。每个带有 virtual 函数的类,都有一个相应的 vtbl,当对象调用某一 virtual 函数,实际被调用的函数取决于该对象的 vptr 所指的哪个 vtbl——编译器在其中寻找适当的函数指针。

virtual 函数的实现并不重要,重要的是如果 Point class 内含 virtual 函数,其对象的体积会增加:在一个 32-bit 架构中,它们将从 64 bits长到 96 bits(两个 ints 加上 vptr);在一个 64-bit 架构中,它们可能从 64 bits 长到 128 bits,因为在这样的架构中指针的大小是 64 bits 的。Point 对象不再适合 64-bit 寄存器。而且,对象在 C++ 和其它语言(比如 C)中,看起来不再具有相同的结构,因为它们在其它语言中的对应物没有 vptr。结果,Points 不再可能传入其它语言写成的函数或从其中传出,除非你为 vptr 做出明确的补偿(这属于实现细节),也因此失去可移植性。

因此,无故地将所有析构函数声明为 virtual ,就像从未声明它们为 virtual 一样,都是错误的。许多人的心得是:只有当 class 内含至少一个 virtual 函数,才为它声明 virtual 析构函数。

标准 string 类型不包含虚拟函数,但是被误导的程序员有时将它当作基类使用:

class SpecialString: public std::string {   // bad idea! std::string has a
... // non-virtual destructor
};

乍看似乎无害,但如果在程序的某个地方因为某种原因,你将一个指向 SpecialString 的指针转型为一个指向 string 的指针,然后你将 delete 施加于这个指向 string 的指针,就会出现“未定义行为”!

SpecialString *pss =   new SpecialString("Impending Doom");
std::string *ps;
...
ps = pss;
...
delete ps; //@ undefined! In practice, *ps's SpecialString resources will be leaked,
//@ because the SpecialString destructor won't be called.

同样的分析适用于任何不带 virtual 析构函数的类,包括全部的 STL 容器类型(例如,vector,list,set,tr1::unordered_map)。

一定不要继承一个标准库容器或任何其它“带有 non-virtual 析构函数”的 class。

有时候希望有抽象 class,但没有任何 pure virtual 函数,则可以为希望成为抽象的 class 声明一个 pure virtual 析构函数。

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

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

AWOV::~AWOV() { } // definition of pure virtual dtor

析构函数的运作方式是:最深层派生(most derived)的那个 class 的析构函数最先被调用,然后是其每一个 base class 的析构函数被调用。编译器会在 AWOV 的 derived class 的析构函数中创建一个对~AWOV()的调用动作,所以必须为这个函数提供一份定义,如果没有定义,链接器会报错。

“给 base class 一个 virtual 析构函数”这个规则只适用于 polymorphic(带多态性质的)base class,这个 base class 的设计目的是为了用来“通过 base class接口处理 derived class对象”。

并非所有 base class 的设计目的都是为了多态用途,如标准 string 和 STL都不被设计为 base class使用,更别提多态了。还有某些 class 的设计目的是作为 base class 使用,但不是为了多态用途。它们都不需要 virtual 析构函数。

总结

<⇧>