来自《Effective C++》条款07:为多态声明virtual析构函数
当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义——实际执行时通常发生的是对象的derived成分没被销毁,而其基类成分通常会被销毁,于是造成了一个诡异的“局部销毁”对象。
解决这个问题的做法很简单:给base class一个virtual析构函数。此后删除derived class对象就会如你想要的那般销毁整个对象。
当class不企图被当作base class,令其析构函数为virtual往往是个馊主意。许多人的心得是:只有当class内含至少一个virtual函数,才能为它声明virtual析构函数。
有时候令class带一个pure virtual析构函数,可能颇为遍历,pure virtual函数导致abstract classes——也就是不能被实体化的class。也就是说不能为那种类型创建对象。然而当你希望拥有抽象class,但手上没有任何pure function时就可以将把希望抽象成的那个class声明为一个pure virtual析构函数。下面是个例子:
class AWOV { public: virtual ~AWOV() == 0; };
这个class有一个pure virtual函数,所以它是一个抽象class,又由于它有个virtual析构函数,所以你不必担心析构函数的问题。然而这里有个窍门:你必须为这个pure virtual析构函数提供一份定义:
AWOV::~AWOV() {}
析构函数的运作方式是,最深层派生的那个class其析构函数最先被调用,然后是其每一个base class的析构函数被调用。编译器会在AWOV的derived classes的析构函数中创建一个对~AWOV的调用动作,所以你必须为这个函数提供一份定义。如果不这样做,连接器会发出抱怨。
请记住:
1. polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
2. Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数。
要分析为什么没有将基类的析构函数声明为virtual,只有基类部分会被销毁呢?这要首先明白C++中的名字查找与继承。理解函数调用的解析过程对于理解C++的继承至关重要,假设我们调用p->mem()(或者obj.mem()),则依次执行以下4个步骤:
1.首先确定p(或obj)的静态类型。因为我们调用的是一个成员,所以该类型必然是类类型。
2. 在p(或obj)的静态类型对应的类中查找mem。如果找不到,则依次在直接基类中不断查找直到到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器将报错。
3. 一旦找到了mem,就进行常规的类型检查以确认对于当前找到的mem,本次调用是合法的。
4. 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:
——如果mem是虚函数且我们是通过引用或指针进行进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,一句是对象的动态类型。
——如果mem不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器将产生一个常规函数调用。
即
通过对象调用虚函数时,调用哪个类的函数取决于对象的类型。对象类型是基类时,就调用基类的函数;对象类型是子类时,就调用子类的函数。
使用指针访问非虚函数时,编译器根据指针本身的类型决定要调用哪个函数,而不是根据指针指向对象类型。
使用指针访问虚函数时,编译器根据指针所指对象的类型决定要调用哪个函数,而与指针本身类型无关。
可以看到如果我们将基类的指针指向动态分配的子类对象,而基类的析构函数没有声明为虚函数,那么按照名字查找就不会调用到派生类的析构函数。
参考资料:
1. 《Effective C++ 》第三版 电子工业出版社
2. 《C++ Primer》第五版 电子工业出版社 15.6