一、为什么使用虚拟析构函数
在c++中,为了实现里氏替换原则,父子类可以通过虚拟函数来实现特异化的不同,即常说的动态绑定机制。正常情况下,构造函数是无法声明成virtual的,但是一般来说,推荐析构函数默认为virtual。也就是说,析构函数使用虚拟函数是常态情况,不使用是非常态。那么,为什么析构函数要声明为virtual呢?
c++是一种静态编译语言,也就是说,绝大多数情况下,在编译期,各编译单元就完成了编译的运作,在实际运行时,装载器只是装载链接相关地址即可。但是由于c++支持动态绑定,即在运行期通过虚表来判断执行为子类对象还是父类对象,这样,就出现了一个问题。如果在父类和子类对象中分配了内存,需要在析构函数时释放,那么,什么时候儿调用父类和子类的析构函数就成为了一个需要考虑的情况。而此时如果指针声明为父类对象,但指向对象是子类对象的话,在释放对象时,就可能会造成意外内存的泄露。(只会调用父类的析构函数而不会调用子类的析构函数)。
如果将子类的析构函数声明为virtual时,就会直接调用子类对象的析构函数,子类对象在析构时,又会自动调用父类对象的析构函数,达到了父子类中内存资源的释放,而这正是程序开发过程中需要的。
二、虚析构函数的使用
千言万语,不如看一个具体的例子:
#include <iostream>
#include <stdlib.h>
class Base
{
public:
Base(){}
~Base(){ std::cout<<"调用Bass析构"<<std::endl;}
//virtual ~Base(){ cout<<"调用基类析构"<<endl;}
};
class Son:public Base
{
public:
Son()
{
p=(char*)malloc(20);
}
~Son()
{
free(p);
std::cout<<"Son析构函数"<<std::endl;
}
private:
char * p;
};
int main(int argc, char * argv[])
{
Base * pBase=new Son();
if (NULL == pBase)
{
std::cout<<"类创建失败"<<std::endl;
exit(1);
}
delete pBase;
return 0;
}
在Base类中,如果去除注释,那么就会是显示调用了子类的析构,否则就会发生内存泄露。但是,大家又知道,使用virtual会增加程序的运行的开销,那么就产生了一个问题,是不是必须要把父类的析构函数一定写virtual呢?这里友情提示一下,这个virtual关键字是具有传染性的,一旦父类声明后,其以后无论继承多少层次,其子类都自动带有virtual属性。
那么什么情况下可以不使用virtual关键字呢?
1、没有继承,没有继承要不要这个关键字没意义。
2、继承的目的单纯,比如只是为了继承原生类型数据,不体现多态;或者只形式化功能不对外暴露接口;私有化继承等(但是这种要防止后期的扩展和误用)
3、使用了c++11后的final关键字,这个和第一条类似。
其实上面的总结就一个,那就是如果析构函数啥都不需要干,那么就可以不声明为virtual.
反过来,使用的情况很简单:
只要继承中其它类函数有了virtual,一定把析构函数也声明为virtual.
总体把握的原则是,如果你无法明确控制是否可以使用virtual,建议直接给个virtual.
三、总结
可能有人会对这种问题不屑一顾,这种小问题还当回事儿?但是许多新人,甚至写了多年c++代码的程序员,在遇到这种问题时,很难快速定位问题。常说的防御性编程,这里就是一种体现。在多年的实践经验中,遇到这种问题还真是不少,希望能引起重视。还是那句话,不纠缠细节,但要重视细节。