上节我们讲到,构造函数不能是虚函数,因为派生类不能继承基类的构造函数,将构造函数声明为虚函数没有意义。
这是原因之一,另外还有一个原因:C++ 中的构造函数用于在创建对象时进行初始化工作,在执行构造函数之前对象尚未创建完成,虚函数表尚不存在,也没有指向虚函数表的指针,所以此时无法查询虚函数表,也就不知道要调用哪一个构造函数。下节将会讲解虚函数表的概念。
析构函数用于在销毁对象时进行清理工作,可以声明为虚函数,而且有时候必须要声明为虚函数。
为了说明虚析构函数的必要性,请大家先看下面一个例子:
#include <iostream>
using namespace std;
//基类
class Base
{
public:
Base();
~Base();
protected:
char *str;
};
Base::Base()
{
str = new char[100];
cout<<"Base constructor"<<endl;
}
Base::~Base()
{
delete[] str;
cout<<"Base destructor"<<endl;
}
//派生类
class Derived: public Base
{
public:
Derived();
~Derived();
private:
char *name;
};
Derived::Derived()
{
name = new char[100];
cout<<"Derived constructor"<<endl;
}
Derived::~Derived()
{
delete[] name;
cout<<"Derived destructor"<<endl;
}
int main()
{
Base *pb = new Derived();
delete pb;
cout<<"-------------------"<<endl;
Derived *pd = new Derived();
delete pd;
return 0;
}
运行结果:
Base constructor
Derived constructor
Base destructor
-------------------
Base constructor
Derived constructor
Derived destructor
Base destructor
本例中定义了两个类,基类 Base 和派生类 Derived,它们都有自己的构造函数和析构函数。在构造函数中,会分配 100 个 char 类型的内存空间;在析构函数中,会把这些内存释放掉。
pb、pd 分别是基类指针和派生类指针,它们都指向派生类对象,最后使用 delete 销毁 pb、pd 所指向的对象。
从运行结果可以看出,语句delete pb;
只调用了基类的析构函数,没有调用派生类的析构函数;而语句delete pd;
同时调用了派生类和基类的析构函数。
在本例中,不调用派生类的析构函数会导致 name 指向的 100 个 char 类型的内存空间得不到释放;除非程序运行结束由操作系统回收,否则就再也没有机会释放这些内存。这是典型的内存泄露。
1) 为什么delete pb;
不会调用派生类的析构函数呢?
因为这里的析构函数是非虚函数,通过指针访问非虚函数时,编译器会根据指针的类型来确定要调用的函数;也就是说,指针指向哪个类就调用哪个类的函数,这在前面的章节中已经多次强调过。pb 是基类的指针,所以不管它指向基类的对象还是派生类的对象,始终都是调用基类的析构函数。
2) 为什么delete pd;
会同时调用派生类和基类的析构函数呢?
pd 是派生类的指针,编译器会根据它的类型匹配到派生类的析构函数,在执行派生类的析构函数的过程中,又会调用基类的析构函数。派生类析构函数始终会调用基类的析构函数,并且这个过程是隐式完成的,这在《C++析构函数》一节中已经讲到了。
更改上面的代码,将基类的析构函数声明为虚函数:
class Base
{
public:
Base();
virtual ~Base();
protected:
char *str;
};
运行结果:
Base constructor
Derived constructor
Derived destructor
Base destructor
-------------------
Base constructor
Derived constructor
Derived destructor
Base destructor
将基类的析构函数声明为虚函数后,派生类的析构函数也会自动成为虚函数。这个时候编译器会忽略指针的类型,而根据指针的指向来选择函数;也就是说,指针指向哪个类的对象就调用哪个类的函数。pb、pd 都指向了派生类的对象,所以会调用派生类的析构函数,继而再调用基类的析构函数。如此一来也就解决了内存泄露的问题。
在实际开发中,一旦我们自己定义了析构函数,就是希望在对象销毁时用它来进行清理工作,比如释放内存、关闭文件等,如果这个类又是一个基类,那么我们就必须将该析构函数声明为虚函数,否则就有内存泄露的风险。也就是说,大部分情况下都应该将基类的析构函数声明为虚函数。
注意,这里强调的是基类,如果一个类是最终的类,那就没必要再声明为虚函数了。