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果然與衆不同,哈哈。