Item 7: 在多态基类中将析构函数声明为虚函数

多态基类应该声明虚拟析构函数

有很多方法取得时间,所以有必要建立一个基类,并为不同的计时方法建立派生类:

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

class AtomicClock: public TimeKeeper { ... };
class WaterClock: public TimeKeeper { ... };
class WristWatch: public TimeKeeper { ... };

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

getTimeKeeper 返回一个指向派生类对象的指针(比如 AtomicClock),那个对象经由一个基类指针也就是一个 TimeKeeper* pointer 被删除,而且这个基类非虚拟析构函数。祸端就在这里,因为 C++ 规定:当一个派生类对象通过使用一个指向带有非虚拟析构函数的基类的指针被删除,则结果是未定义的。 运行时比较典型的后果是这个对象的派生部分不会被析构。如果 getTimeKeeper 返回一个指向 AtomicClock 对象的指针,则对象的 AtomicClock 部分也就是在 AtomicClock class 中声明的数据成员很可能不会被析构,AtomicClock 的析构函数也不会运行。然而,基类部分(也就是 TimeKeeper 部分)很可能已被析构,这就导致了一个古怪的“部分被析构”对象。这是一个导致泄漏资源。

消除这个问题很简单:给基类一个虚拟析构函数。于是,删除一个派生类对象的时候就有了你所期望的行为。将析构整个对象,包括全部的派生类构件:

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

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

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

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

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 (虚拟函数表指针)的指针的形式。vptr 指向一个被称为 vtbl (虚拟函数表)的函数指针数组,每一个带有虚拟函数的类都有一个相关联的 vtbl。当在一个对象上调用虚拟函数时,实际的被调用函数通过下面的步骤确定:找到对象的 vptr 指向的 vtbl,然后在 vtbl 中寻找合适的函数指针。

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

最终结果就是无故地将所有析构函数声明为虚拟,和从不把它们声明为虚拟一样是错误的。

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

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

一眼看上去,这可能无伤大雅,但是,如果在程序的某个地方因为某种原因,你将一个指向 SpecialString 的指针转型为一个指向 string 的指针,然后你将 delete 施加于这个指针,你就立刻被放逐到未定义行为的领地:

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.

同样的分析可以适用于任何缺少虚拟析构函数的类,包括全部的 STL 容器类型(例如,vector,list,set,tr1::unordered_map。如果你受到从标准容器或任何其它带有非虚拟析构函数的类继承的诱惑,一定要挺住!

有时候,给一个类提供一个纯虚拟析构函数能提供一些便利。

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

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

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

总结

  • 多态基类应该声明虚拟析构函数。如果一个类有任何虚拟函数,它就应该有一个虚拟析构函数。
  • 不是设计用来作为基类或不是设计用于多态的类就不应该声明虚拟析构函数。
posted @ 2020-01-02 22:57  刘-皇叔  阅读(128)  评论(0编辑  收藏  举报