class Base
{
public:
Base();
~Base();
…
};
class Derived : public Base
{
…
};
Base *p = new Derived;
delete p;
这个例子中,基类指针p指向派生类对象,那么经由基类指针来释放该派生类对象,会导致资源泄漏。因为基类的析构函数不是虚的,delete p只会调用基类的析构函数,派生类的析造函数不会被调用。解决方法是将基类的析构函数改成虚析构函数。
那么什么时候该用虚析构函数呢?任何一个类只要带有virtual函数,这意味着该类被设计为多态用途的基类,那么基本上这个类也应该有一个virtual析构函数。如果一个类不含virtual函数,通常表示它的设计意图是不被用作多态基类。当类不被当作基类或者不被用作多态性,令其析构函数为virtual是不好的做法。因为一个类有虚函数,类对象会增加一个虚表指针(virtual table pointer)来决定运行期间哪个虚函数该被调用。
只有析构函数应该定义为虚函数,构造函数不能定义为虚函数。构造函数是在对象构造前运行的,在构造函数运行时,对象的动态类型还不完整。
如果基类成员与派生类成员接受的实参不同,就没有办法通过基类类型的引用或指针调用派生类函数,如下例:
class Base{
public:
virtual void fcn()
{
cout<<"Base::fcn()"<<endl;
}
};
class D1 : public Base{
public:
void fcn(int){ //参数列表不同于基类
cout<<"D1::fcn(int)"<<endl;
}
};
class D2 : public D1{
public:
void fcn(int){ //非虚函数,屏蔽了D1:fcn(int)
cout<<"D2::fcn(int)"<<endl;
}
void fcn(){ //虚函数
cout<<"D2::fcn()"<<endl;
}
};
int main()
{
Base bobj;D1 d1obj;D2 d2obj;
Base * bp1 = &bobj, *bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn();
bp2->fcn();
bp3->fcn();
return 0;
}
D1中的fcn版本没有重定义Base的虚函数fcn,相反,他/她屏蔽了基类的虚函数fcn。结果是D1有两个名为fcn的函数版本:类从基类继承了一个名为fcn的虚函数,类又定义了一个自己的名为fcn非虚成员函数,该函数接受一个int参数。但是从Base继承的虚函数不能通过D1对象(或D1的引用指针)调用,因为该函数被fcn(int)的定义屏蔽了。
通过基类类型的应用或指针调用函数时,编译器将在基类中查找该函数而不忽略派生类:
Base bobj; D1 d1obj; D2 d2obj;
Base *bp1 = &bobj, *bp2 = &d1obj, *bp2 = &d2obj;
bp1->fcn(); //调用Base::fcn
bp2->fcn();//调用Base::fcn
bp3->fcn();//调用D2::fcn
三个指针都是基类类型的指针,因此通过在Base中查找fcn来确定这个三个调用,所以这些调用是合法的。因为fcn是虚函数会采用运行时绑定实际类型。bp2基本对象是D1类,D1类没有重定义不接受参数的虚函数版本,通过bp2的函数调用会调用Base类中的定义版本。bp3重定义了fcn所以调用自己的虚函数。
另外析构函数还可以是纯虚析构函数。具有纯虚函数的为表示抽象类,如果某个类没有任何一个成员函数,又想将它设计成抽象类,就只能将析构函数作为纯虚的。如:
class Test
{
public:
virtual ~Test() = 0;
}
// 虚析构函数的定义
virtual ~Test()
{
}
通常纯虚函数不需要提供定义,但是这边有个例外,纯虚析构函数必需给出实现体才能通过编译链接。析构函数析构函数析构的次序是最深层的派生类的析构函数先被调用,然后依次调用每一层的析构函数。所以编译器会在析构Test类的派生类的析构函数中创建一个对~Test()的调用动作,所以必须提供这个析构函数的定义。