条款07:为多态基类声明virtual析构函数
Declare destructors virtual in polymorphic base classes
该条款内容较多,分成两章来进行学习记录。
Virtual析构函数
首先,也是从一个例子入手。
对于时间的记录,可以有许多种方法。因此,设计一个TimeKeeper base class和一些derived classes以作为不同的计时方法是一种比较可取的方法:
class TimeKeeper { //base class
public:
TimeKeeper();
~TimeKeeper(); //Non-vitrual的析构函数
...
};
class AtomicClock : public TimeKeeper { ... } //原子钟
class WaterClock: public TimeKeeper { ... } //水钟
class WristWatch: public TimeKeeper { ... } //腕表
在使用的过程中,用户可能只想在程序中使用时间, 而并不想操心时间的计算细节。
因此,这个时候,我们可以设计factory(工厂)函数,返回指针指向一个计时对象。即:
- Factory函数会“返回一个base class指针, 指向新生成的derived class对象”。
TimeKeeper* getTimeKeeper();
为了遵守factory函数的规则,被getTimeKeeper()返回的对象必须位于heap(堆)。因此为了避免泄露内存和其他资源,将factory函数返回的每一个对象适当的delete掉非常重要:
TimeKeeper* ptk = getTimeKeeper(); //从TimeKeeper继承体系中获取一个动态分配对象
... //使用这个对象
delete ptk; //释放这个对象,避免资源泄露
但是,在上述的代码中,纵使使用者把每一件事都做对了,仍然没有办法知道程序如何行动!
原因在于:
- getTimeKeeper返回的指针指向一个derived class对象(例如AtomicClock),而这个对象却经由一个base class指针(例如TimeKeeper*指针)删除。但是,目前的base class(TimeKeeper)有一个non-virtual析构函数。
之所以会引来错误,是因为C++明确指出,当derived class对象经由一个base class指针被删除,而该base class又带有一个non-virtual析构函数,这样操作的结果并没有定义:
- 实际执行时通常发生的是对象的derived成分没有被销毁。
也就是说,如果getTimeKeeper返回指针指向一个AtomicClock对象,其中的AtomicClock成分(即声明于AtomicClock class内的成员变量)很可能没有被销毁,而AtomicClock的析构函数也未能执行。
然而,其中的base class成分(即TimeKeeper部分)却通常会被销毁,于是就造成了一种“局部销毁”对象。
解决办法:
-
给base class一个virtual析构函数。
此后,删除derived class对象,就会销毁整个对象,包括所有的derived class的成分:
class TimeKeeper { //base class
public:
TimeKeeper();
virtual ~TimeKeeper(); //vitrual的析构函数
...
};
TimeKeeper* ptk = getTimeKeeper();
...
delete ptk;
像TimeKeeper这样的base classes除了析构函数之外通常还有其他的virtual函数,因为:
- virtual函数的目的是允许derived class的实现得以客制化。
例如TimeKeeper就可能拥有一个virtual getCurrentTime,它在不同的derived classes中有不同的实现代代码。任何class只要带有virtual函数,就几乎可以确定也有一个virtual析构函数。
基类指针可以指向派生类的对象(多态性),如果删除该指针delete []p;就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全。所以,将析构函数声明为虚函数是十分必要的。
1.每个析构函数(不加 virtual) 只负责清除自己的成员。
2.可能有基类指针,指向的确是派生类成员的情况。(这是很正常的)
那么当析构一个指向派生类成员的基类指针时,程序就不知道怎么办了。
所以要保证运行适当的析构函数,基类中的析构函数必须为虚析构。