多态的特点是:调用相同的接口,程序的运行时行为取决于动态绑定在这个指针或者引用上的实现。例如,为了抽象出获取时间的方法,可以设计一个基类用于规定一个统一的接口,而不同的派生类用于实现各自的计时方式:
class Timer
{
public:
Timer();
~Timer();
//...
};
class UTCTimer : public Timer
{
public:
//...
};
class AtomicTimer : public Timer
{
public:
//...
};
此时多态的特性就凸显出来了,可以让一个父类的指针或者引用指向子类对象。这样一来,就实现了动态绑定,timer
的具体行为将取决于实际绑定对象的实现。
int main()
{
Timer *timer1 = new UTCTimer;
Timer *timer2 = new AtomicTimer;
//...
delete timer1;
delete timer2;
return 0;
}
现在的问题出在删除对象时。多态指针的类型是Timer
,然而其实际类型确是子类的实现类型。所以,在delete timer1
和delete timer2
时,仅仅调用了基类Timer
的析构函数,而并没有调用实现类的析构函数。结果时,对象将处于一个诡异的“半销毁”状态:其基类部分确实被销毁了,然而派生类部分依旧完好无损。
消除这个问题的方式很简单:给基类Timer
实现一个虚析构函数。如此一来,无论是基类对象,还是派生类对象,都会被全部销毁掉:
class Timer
{
public:
Timer();
virtual ~Timer();
//...
};
反过来看,如果一个类不含虚函数,通常就表示它不应该被继承。
实际的使用过程中,使用虚函数需要慎重。因为编译器实现虚函数确确实实是存在一些开销的。例如,为了实现虚函数和动态绑定,每个对象都必须携带某些额外信息,用于在运行期决定应该动态地调用哪个函数。这份信息通常是由一个vptr
指针指出的;vptr
指向一个由函数指针组成的数组,这个数组被称为vtbl
。当对象调用某一个虚函数时,实际的行为取决于该对象的vptr
所指的那个vtbl
————编译器在其中寻找适当的函数指针。如此一来,每个对象的体积都会增加(至少需要增加一个指针的大小)。
vtbl
是虚函数表,vptr
是虚函数表指针。
因此,无端地将所有类的析构函数都声明为虚函数,就同永远不定义虚函数一样错误。许多人的心得是:只有当类中至少存在一个虚函数(或者纯虚函数)时,才将其析构函数声明为虚函数。
并非所有基类的设计都是用于多态用途。例如,STL
中几乎所有的容器都不被设计为基类使用,更不用提多态了;还有一些类(例如input_iterator_tag
),它们不是用来实现父类的成员函数的,因此它们也不需要虚析构函数。
【注意】
- 用于多态目的的基类,必须含有一个虚析构函数。
- 如果不是用来实现多态的基类,就不应该包含虚析构函数。