c++虚函数动态联编需要避免的内存泄漏问题
近期项目中跟其他开发组共同开发,底层设备控制模块的同事用C++设计了一个虚类接口,接口定义如下
class MyInterface {
public:
virtual void function() = 0;
};
然后在继承实现接口时用指针动态联编delete,即
class MySubClass : public MyInterface {
// 实现接口内容
};
// 如下使用
MyInterface *p = new MySubClass();
// 使用p做一系列操作
delete p; p = NULL;
这时候编译器编译时时会提示
warning: delete called on that is abstract but has non-virtual destructor [-Wdelete-non-virtual-dtor]
同事也是大神,平时c语言开发习惯了,压根不看编译器警告,一直没有处理。
当我开始jni集成是,我编译一些代码,然后瞄到了这一条警告,Oh, my god!这不是分分钟内存泄漏的节奏吗。同事也是心大,没用过c++也不好好看一下继承有哪些坑。
接下来我们通过分别展示这代码为什么会有问题。
一、构造函数和析构函数
构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。
析构函数(destructor) 与构造函数相反,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统自动执行析构函数。析构函数往往用来做“清理善后” 的工作(例如在建立对象时用new开辟了一片内存空间,delete会自动调用析构函数后释放内存)。
二、两个继承例子
1. 普通析构函数
class MyInterface {
public:
virtual void function() = 0;
};
// 非虚函数声明的析构函数基类(父类)
class BaseClassNormalDestructor {
public:
BaseClassNormalDestructor() {
printf("BaseClassNormalDestructor constructor()\n");
};
~BaseClassNormalDestructor() {
printf("BaseClassNormalDestructor destructor()\n");
};
virtual void function() = 0;
};
// 非虚函数声明的析构函数派生类(子类)
class ChildClassNormalDestructor: public BaseClassNormalDestructor {
public:
ChildClassNormalDestructor() {
printf("ChildClassNormalDestructor constructor() \n");
};
~ChildClassNormalDestructor() {
printf("ChildClassNormalDestructor destructor() \n");
};
virtual void function() {
printf("ChildClassNormalDestructor::function\n");
};
};
上面的例子中,我们定义了一个子类,子类的析构函数是普通函数,非虚函数,子类ChildClassNormalVirtualDestructor继承于BaseClassNormalVirtualDestructor 并且实现了接口function()
我们使用ChildClassNormalVirtualDestructor 有两种方式
方式一、子类直接使用,不会用指针动态转换
printf("==============example of ChildClassNormalDestructor stack mode===============\n");
{
ChildClassNormalDestructor child;
child.function();
}
printf("\n\n");
printf("==============example of ChildClassNormalDestructor pointer mode===============\n");
ChildClassNormalDestructor *pchild = new ChildClassNormalDestructor();
pchild->function();
delete pchild; pchild = 0;
printf("\n\n");
输出结果
==============example of ChildClassNormalDestructor stack mode===============
BaseClassNormalDestructor constructor()
ChildClassNormalDestructor constructor()
ChildClassNormalDestructor::function
ChildClassNormalDestructor destructor()
BaseClassNormalDestructor destructor()
==============example of ChildClassNormalDestructor pointer mode===============
BaseClassNormalDestructor constructor()
ChildClassNormalDestructor constructor()
ChildClassNormalDestructor::function
ChildClassNormalDestructor destructor()
BaseClassNormalDestructor destructor()
我们可以看到,释放内存时先调用子类ChildClassNormalDestructor的析构函数,再调用父类的析构函数BaseClassNormalDestructor,这时候父类和子类准备在析构函数释放资源的操作都能够被正确执行。
方式二、动态联编,用父类的指针指向子类实例地址
printf("==============example of BaseClassNormalDestructor===============\n");
BaseClassNormalDestructor *pNormalVirtualDestructor = new ChildClassNormalDestructor();
pNormalVirtualDestructor->function();
delete pNormalVirtualDestructor; pNormalVirtualDestructor = 0;
printf("\n\n");
执行结果
==============example of BaseClassNormalDestructor===============
BaseClassNormalDestructor constructor()
ChildClassNormalDestructor constructor()
ChildClassNormalDestructor::function
BaseClassNormalDestructor destructor()
此时就悲剧啦,释放内存时只调用了父类BaseClassNormalDestructor的析构函数,子类ChildClassNormalDestructor析构函数没有调用到,那么就会造成本来需要在子类ChildClassNormalDestructor析构函数释放的资源没有得到正确释放,进而造成内存泄漏。
解决方案
有问题就要探讨解决方案,没有解决方案的就必须规避问题的出现。
解决方案一:在delete时进行类型强转,将动态联编的基类指针转换为原来子类的指针类型,再执行delete
调用方法
printf("==============example of (Convertion)BaseClassNormalDestructor===============\n");
BaseClassNormalDestructor *pCovertionNormalVirtualDestructor = new ChildClassNormalDestructor();
pCovertionNormalVirtualDestructor->function();
delete ((ChildClassNormalDestructor*)pCovertionNormalVirtualDestructor); pCovertionNormalVirtualDestructor = 0;
printf("\n\n");
输出结果
==============example of (Convertion)BaseClassNormalDestructor===============
BaseClassNormalDestructor constructor()
ChildClassNormalDestructor constructor()
ChildClassNormalDestructor::function
ChildClassNormalDestructor destructor()
BaseClassNormalDestructor destructor()
可以看到在delete时pCovertionNormalVirtualDestructor先强制转换为子类(ChildClassNormalDestructor*)指针,这样系统释放内存时就会先调用子类的析构函数,然后在调用基类的构造函数;但是这种方法有一个严重的缺陷,必须知道基类真正指向的子类类型,这就与动态联编的出发点相违背,动态联编就是为了让调用者不需关心真正的之类是什么,只需要知道接口实现了对应功能就行。例如设计模式中的抽象工厂。
解决方案二(最佳解决方案)、基类声明析构函数为virtual
// 虚函数声明的析构函数基类(父类)
class BaseClassVirtualDestructor {
public:
BaseClassVirtualDestructor() {
printf("BaseClassVirtualDestructor constructor()\n");
};
virtual ~BaseClassVirtualDestructor() {
printf("BaseClassVirtualDestructor destructor()\n");
};
virtual void function() = 0;
};
// 虚函数声明的析构函数派生类(子类)
class ChildClassVirtualDestructor: public BaseClassVirtualDestructor {
public:
ChildClassVirtualDestructor() {
printf("ChildClassVirtualDestructor constructor() \n");
};
~ChildClassVirtualDestructor() {
printf("ChildClassVirtualDestructor destructor() \n");
};
virtual void function() {
printf("ChildClassVirtualDestructor::function\n");
};
};
调用
printf("==============example of BaseClassVirtualDestructor===============\n");
BaseClassVirtualDestructor *pVirtualDestructor = new ChildClassVirtualDestructor();
pVirtualDestructor->function();
delete pVirtualDestructor; pVirtualDestructor = 0;
输出结果
==============example of BaseClassVirtualDestructor===============
BaseClassVirtualDestructor constructor()
ChildClassVirtualDestructor constructor()
ChildClassVirtualDestructor::function
ChildClassVirtualDestructor destructor()
BaseClassVirtualDestructor destructor()
可以不需要像解决方案一强转指针类型再delete释放内存。原理就是通过virtual声明将析构函数加入虚函数表,这样在动态联编模式下能够正确释放内存有效避免内存泄漏。
当然这次开发最后就是发review代码,然后提出评审意见最后把析构函数声明为virtual,大家愉快的解决问题。这里就涉及到习惯,一定要保持怀疑的眼光去看待代码,无论是自己写的代码还是别的开发者写的代码,否则遇到坑都不知道自己怎么被埋的。
附录一、完整代码
main.cpp
#include <cstdio>
class MyInterface {
public:
virtual void function() = 0;
};
// 非虚函数声明的析构函数基类(父类)
class BaseClassNormalDestructor {
public:
BaseClassNormalDestructor() {
printf("BaseClassNormalDestructor constructor()\n");
};
~BaseClassNormalDestructor() {
printf("BaseClassNormalDestructor destructor()\n");
};
virtual void function() = 0;
};
// 非虚函数声明的析构函数派生类(子类)
class ChildClassNormalDestructor: public BaseClassNormalDestructor {
public:
ChildClassNormalDestructor() {
printf("ChildClassNormalDestructor constructor() \n");
};
~ChildClassNormalDestructor() {
printf("ChildClassNormalDestructor destructor() \n");
};
virtual void function() {
printf("ChildClassNormalDestructor::function\n");
};
};
// 虚函数声明的析构函数基类(父类)
class BaseClassVirtualDestructor {
public:
BaseClassVirtualDestructor() {
printf("BaseClassVirtualDestructor constructor()\n");
};
virtual ~BaseClassVirtualDestructor() {
printf("BaseClassVirtualDestructor destructor()\n");
};
virtual void function() = 0;
};
// 虚函数声明的析构函数派生类(子类)
class ChildClassVirtualDestructor: public BaseClassVirtualDestructor {
public:
ChildClassVirtualDestructor() {
printf("ChildClassVirtualDestructor constructor() \n");
};
~ChildClassVirtualDestructor() {
printf("ChildClassVirtualDestructor destructor() \n");
};
virtual void function() {
printf("ChildClassVirtualDestructor::function\n");
};
};
int main(void) {
printf("==============example of ChildClassNormalDestructor stack mode===============\n");
{
ChildClassNormalDestructor child;
child.function();
}
printf("\n\n");
printf("==============example of ChildClassNormalDestructor pointer mode===============\n");
ChildClassNormalDestructor *pchild = new ChildClassNormalDestructor();
pchild->function();
delete pchild; pchild = 0;
printf("\n\n");
printf("==============example of BaseClassNormalDestructor===============\n");
BaseClassNormalDestructor *pNormalVirtualDestructor = new ChildClassNormalDestructor();
pNormalVirtualDestructor->function();
delete pNormalVirtualDestructor; pNormalVirtualDestructor = 0;
printf("\n\n");
printf("==============example of (Convertion)BaseClassNormalDestructor===============\n");
BaseClassNormalDestructor *pCovertionNormalVirtualDestructor = new ChildClassNormalDestructor();
pCovertionNormalVirtualDestructor->function();
delete ((ChildClassNormalDestructor*)pCovertionNormalVirtualDestructor); pCovertionNormalVirtualDestructor = 0;
printf("\n\n");
printf("==============example of BaseClassVirtualDestructor===============\n");
BaseClassVirtualDestructor *pVirtualDestructor = new ChildClassVirtualDestructor();
pVirtualDestructor->function();
delete pVirtualDestructor; pVirtualDestructor = 0;
return 0;
}
附录二
写博客的时候发现一个有趣的现象,在不同平台上编译运行结果不一样
在linux上用g++编译器编译运行
cplusplus_virtual_constructor]$ ./a.out
==============example of ChildClassNormalDestructor stack mode===============
BaseClassNormalDestructor constructor()
ChildClassNormalDestructor constructor()
ChildClassNormalDestructor::function
ChildClassNormalDestructor destructor()
BaseClassNormalDestructor destructor()
==============example of ChildClassNormalDestructor pointer mode===============
BaseClassNormalDestructor constructor()
ChildClassNormalDestructor constructor()
ChildClassNormalDestructor::function
ChildClassNormalDestructor destructor()
BaseClassNormalDestructor destructor()
==============example of BaseClassNormalDestructor===============
BaseClassNormalDestructor constructor()
ChildClassNormalDestructor constructor()
ChildClassNormalDestructor::function
BaseClassNormalDestructor destructor()
==============example of (Convertion)BaseClassNormalDestructor===============
BaseClassNormalDestructor constructor()
ChildClassNormalDestructor constructor()
ChildClassNormalDestructor::function
ChildClassNormalDestructor destructor()
BaseClassNormalDestructor destructor()
==============example of BaseClassVirtualDestructor===============
BaseClassVirtualDestructor constructor()
ChildClassVirtualDestructor constructor()
ChildClassVirtualDestructor::function
ChildClassVirtualDestructor destructor()
BaseClassVirtualDestructor destructor()
在苹果macos的g++编译器编译运行没有声明virtual析构函数时会崩溃
cplusplus_virtual_constructor$ ./a.out
==============example of ChildClassNormalDestructor stack mode===============
BaseClassNormalDestructor constructor()
ChildClassNormalDestructor constructor()
ChildClassNormalDestructor::function
ChildClassNormalDestructor destructor()
BaseClassNormalDestructor destructor()
==============example of ChildClassNormalDestructor pointer mode===============
BaseClassNormalDestructor constructor()
ChildClassNormalDestructor constructor()
ChildClassNormalDestructor::function
ChildClassNormalDestructor destructor()
BaseClassNormalDestructor destructor()
==============example of BaseClassNormalDestructor===============
BaseClassNormalDestructor constructor()
ChildClassNormalDestructor constructor()
ChildClassNormalDestructor::function
Illegal instruction: 4
可以看到Illegal instruction: 4崩溃了,macos果然与众不同,哈哈。