这是因为,通过基类指针来销毁派生类对象这个行为,当基类没有虚析构函数时会产生问题。我们知道删除指针对象是没有问题的,指针对象的析构函数会正确调用,但仅限于指针的类型所表示的对象大小。如果以一个基类指针指向其派生类,删除这个基类指针只能删除基类对象部分,而不能删除整个派生类对象,原因是通过基类指针无法访问派生类的析构函数。
但是,如果像其它虚函数一样,基类的析构函数也是虚的,那么派生类的析构函数也必然是虚的,删除基类指针时,它就会通过虚函数表找到正确的派生类析构函数并调用它,从而正确析构整个派生类对象。
这点就像我们生成一个动态数组,然后释放它的空间一样。是整个数组而不是其中一个元素的空间要释放。如:
char *itsString = new char [2];
delete [ ] itsString;
要加上 [ ] ,表示删除整个数组,如果不加上只会删除 itsString 的第一个元素。
delete 这里的删除指的是 释放所占用的内存空间。
打个比方,如果基类指针相当于 itsString ,那么整个类就像 [ ] itsString 。而我们真正想析构的是整个对象而不是对象的一部分。
我们来看看代码:
#include <iostream> using namespace std; class CBase { public: CBase() { cout << "CBase Constructor...\n"; } virtual ~CBase() { cout << "CBase Destructor...\n"; } //指定基类析构函数为虚的 virtual void display() { cout << "Hello, World! CBase \n"; } //虚函数 void Hello() { cout << "Call CBase Hello.\n"; } //非虚函数 }; class CDerived: public CBase { public: CDerived() { cout << "CDerived Constructor...\n"; } ~CDerived() { cout << "CDerived Destructor...\n"; } //这里也是虚析构函数,因为基类中析构函数是虚的 void display() { cout << "Hello, World! CDerived \n"; } //虚函数,覆盖基类中的虚函数。 void Hello() { cout << "Call CDerived Hello.\n"; } //非虚函数 }; void main() { CDerived Derived; //栈中对象 CBase *pBase = new CDerived; // 以基类指针指向派生类对象。这里是堆中对象 cout << "Derived.Hello(); \n\t"; Derived.Hello(); cout << "pBase->Hello(); \n\t"; // 因为 Hello() 为 非虚函数,只能访问基类中的方法。 pBase->Hello(); cout << "Derived.display(); \n\t"; Derived.display(); cout << "pBase->display(); \n\t"; // 因为虚函数的关系,派生类中的方法被正确调用。 pBase->display(); cout << "delete pBase; \n\t"; /* 问题就在这里,如果基类的析构函数不是虚的,那么这里只能析构 CBase 对象,和指针的类型相同。 基类使用虚析构函数后,基类指针才可以(通过虚函数表)访问派生类的虚析构函数,调用派生类的析构函数析构派生类本身,可以看到整个派生类依次被析构掉了。 */ delete pBase; // 基类是虚析构函数,进而派生类析构函数也是虚的,整个派生类被正确析构。 cout << "----------------\n"; }
下面,我在这里提一下另一个关注的问题:
为什么构造函数不能是虚的?
原因是构造自己时,对象还不存在。虚函数需要有虚函数表,但这个表因为在构造阶段是不存在的,至少还没分配内存,无法实现定义要求。