本文針對 linux 下的 C++ 程序的內(nèi)存泄漏的檢測方法及其實現(xiàn)進行探討。其中包括 C++ 中的 new 和 delete 的基本原理,內(nèi)存檢測子系統(tǒng)的實現(xiàn)原理和具體方法,以及內(nèi)存泄漏檢測的高級話題。作為內(nèi)存檢測子系統(tǒng)實現(xiàn)的一部分,提供了一個具有更好的使用特性的互斥體(Mutex)類。
在 windows 下使用 VC 編程時,我們通常需要 DEBUG 模式下運行程序,而后調(diào)試器將在退出程序時,打印出程序運行過程中在堆上分配而沒有釋放的內(nèi)存信息,其中包括代碼文件名、行號以及內(nèi)存大小。該功能是 MFC Framework 提供的內(nèi)置機制,封裝在其類結(jié)構(gòu)體系內(nèi)部。
在 linux 或者 unix 下,我們的 C++ 程序缺乏相應(yīng)的手段來檢測內(nèi)存信息,而只能使用 top 指令觀察進程的動態(tài)內(nèi)存總額。而且程序退出時,我們無法獲知任何內(nèi)存泄漏信息。為了更好的輔助在 linux 下程序開發(fā),我們在我們的類庫項目中設(shè)計并實現(xiàn)了一個內(nèi)存檢測子系統(tǒng)。下文將簡述 C++ 中的 new 和 delete 的基本原理,并講述了內(nèi)存檢測子系統(tǒng)的實現(xiàn)原理、實現(xiàn)中的技巧,并對內(nèi)存泄漏檢測的高級話題進行了討論。
當(dāng)我們在程序中寫下 new 和 delete 時,我們實際上調(diào)用的是 C++ 語言內(nèi)置的 new operator 和 delete operator。所謂語言內(nèi)置就是說我們不能更改其含義,它的功能總是一致的。以 new operator 為例,它總是先分配足夠的內(nèi)存,而后再調(diào)用相應(yīng)的類型的構(gòu)造函數(shù)初始化該內(nèi)存。而 delete operator 總是先調(diào)用該類型的析構(gòu)函數(shù),而后釋放內(nèi)存(圖1)。我們能夠施加影響力的事實上就是 new operator 和 delete operator 執(zhí)行過程中分配和釋放內(nèi)存的方法。
先解決問題1。首先我們可以利用 C 的預(yù)編譯宏 __FILE__ 和 __LINE__,這兩個宏將在編譯時在指定位置展開為該文件的文件名和該行的行號。而后我們需要將缺省的全局 new operator 替換為我們自定義的能夠傳入文件名和行號的版本,我們在子系統(tǒng)頭文件 MemRecord.h 中定義:
#define DEBUG_NEW new(__FILE__, __LINE__ )
而后在所有需要使用內(nèi)存檢測的客戶程序的所有的 cpp 文件的開頭加入
#include "MemRecord.h"
#define new DEBUG_NEW
就可以將客戶源文件中的對于全局缺省的 new operator 的調(diào)用替換為 new (__FILE__,__LINE__) 調(diào)用,而該形式的new operator將調(diào)用我們的operator new (size_t nSize, char* pszFileName, int nLineNum),其中 nSize 是由 new operator 計算并傳入的,而 new 調(diào)用點的文件名和行號是由我們自定義版本的 new operator 傳入的。我們建議在所有用戶自己的源代碼文件中都加入上述宏,如果有的文件中使用內(nèi)存檢測子系統(tǒng)而有的沒有,則子系統(tǒng)將可能因無法監(jiān)控整個系統(tǒng)而輸出一些泄漏警告。
首先,在我們編制 c++ 應(yīng)用時,有時需要在堆上創(chuàng)建單個對象,有時則需要創(chuàng)建對象的數(shù)組。關(guān)于 new 和 delete 原理的敘述我們可以知道,對于單個對象和對象數(shù)組來說,內(nèi)存分配和刪除的動作是大不相同的,我們應(yīng)該總是正確的使用彼此搭配的 new 和 delete 形式。但是在某些情況下,我們很容易犯錯誤,比如如下代碼:
class Test {};
……
Test* pAry = new Test[10];//創(chuàng)建了一個擁有 10 個 Test 對象的數(shù)組
Test* pObj = new Test;//創(chuàng)建了一個單對象
……
delete []pObj;//本應(yīng)使用單對象形式 delete pObj 進行內(nèi)存釋放,卻錯誤的使用了數(shù)
//組形式
delete pAry;//本應(yīng)使用數(shù)組形式 delete []pAry 進行內(nèi)存釋放,卻錯誤的使用了單對
//象的形式
不匹配的 new 和 delete 會導(dǎo)致什么問題呢?C++ 標(biāo)準對此的解答是"未定義",就是說沒有人向你保證會發(fā)生什么,但是有一點可以肯定:大多不是好事情--在某些編譯器形成的代碼中,程序可能會崩潰,而另外一些編譯器形成的代碼中,程序運行可能毫無問題,但是可能導(dǎo)致內(nèi)存泄漏。
既然知道形式不匹配的 new 和 delete 會帶來的問題,我們就需要對這種現(xiàn)象進行毫不留情的揭露,畢竟我們重載了所有形式的內(nèi)存操作 operator new,operator new[],operator delete,operator delete[]。
我們首先想到的是,當(dāng)用戶調(diào)用特定方式(單對象或者數(shù)組方式)的 operator new 來分配內(nèi)存時,我們可以在指向該內(nèi)存的指針相關(guān)的數(shù)據(jù)結(jié)構(gòu)中,增加一項用于描述其分配方式。當(dāng)用戶調(diào)用不同形式的 operator delete 的時候,我們在 map 中找到與該指針相對應(yīng)的數(shù)據(jù)結(jié)構(gòu),然后比較分配方式和釋放方式是否匹配,匹配則在 map 中正常刪除該數(shù)據(jù)結(jié)構(gòu),不匹配則將該數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)移到一個所謂 "ErrorDelete" 的 list 中,在程序最終退出的時候和內(nèi)存泄漏信息一起打印。
上面這種方法是最順理成章的,但是在實際應(yīng)用中效果卻不好。原因有兩個,第一個原因我們上面已經(jīng)提到了:當(dāng) new 和 delete 形式不匹配時,其結(jié)果"未定義"。如果我們運氣實在太差--程序在執(zhí)行不匹配的 delete 時崩潰了,我們的全局對象(appMemory)中存儲的數(shù)據(jù)也將不復(fù)存在,不會打印出任何信息。第二個原因與編譯器相關(guān),前面提到過,當(dāng)編譯器處理自定義數(shù)據(jù)類型或者自定義數(shù)據(jù)類型數(shù)組的 new 和 delete 操作符的時候,通常使用編譯器相關(guān)的 cookie 技術(shù)。這種 cookie 技術(shù)在編譯器中可能的實現(xiàn)方式是:new operator 先計算容納所有對象所需的內(nèi)存大小,而后再加上它為記錄 cookie 所需要的內(nèi)存量,再將總?cè)萘總鹘ooperator new 進行內(nèi)存分配。當(dāng) operator new 返回所需的內(nèi)存塊后,new operator 將在調(diào)用相應(yīng)次數(shù)的構(gòu)造函數(shù)初始化有效數(shù)據(jù)的同時,記錄 cookie 信息。而后將指向有效數(shù)據(jù)的指針返回給用戶。也就是說我們重載的 operator new 所申請到并記錄下來的指針與 new operator 返回給調(diào)用者的指針不一定一致(圖3)。當(dāng)調(diào)用者將 new operator 返回的指針傳給 delete operator 進行內(nèi)存釋放時,如果其調(diào)用形式相匹配,則相應(yīng)形式的 delete operator 會作出相反的處理,即調(diào)用相應(yīng)次數(shù)的析構(gòu)函數(shù),再通過指向有效數(shù)據(jù)的指針位置找出包含 cookie 的整塊內(nèi)存地址,并將其傳給 operator delete 釋放內(nèi)存。如果調(diào)用形式不匹配,delete operator 就不會做上述運算,而直接將指向有效數(shù)據(jù)的指針(而不是真正指向整塊內(nèi)存的指針)傳入 operator delete。因為我們在 operator new 中記錄的是我們所分配的整塊內(nèi)存的指針,而現(xiàn)在傳入 operator delete 的卻不是,所以就無法在全局對象(appMemory)所記錄的數(shù)據(jù)中找到相應(yīng)的內(nèi)存分配信息。
2. 程序的N個線程進行內(nèi)存分配,并將指針傳遞給一個數(shù)據(jù)存儲,由M個線程從數(shù)據(jù)存儲進行數(shù)據(jù)處理和內(nèi)存釋放。由于 N 遠大于M,或者M個線程數(shù)據(jù)處理的時間過長,導(dǎo)致內(nèi)存分配的速度遠大于內(nèi)存被釋放的速度。但是在程序退出的時候,數(shù)據(jù)存儲中的指針值所指向的內(nèi)存塊被依次釋放。
class B {…};
class A {
public:
A() {m_pB = NULL};
A(B* pb) {m_pB = pb;};
~A()
{
if (m_pB != NULL)
行號1 delete m_pB; //這句最要命
};
private:
class B* m_pB;
……
}
int main()
{
A* pA = new A(new B);
……
行號2 delete pA;
}
在上述代碼中,main 函數(shù)中的一句 delete pA 我們稱之為"嵌套刪除",即我們 delete A 對象的時候,在A對象的析構(gòu)執(zhí)行了另一個 delete B 的動作。當(dāng)用戶使用我們的內(nèi)存檢測子系統(tǒng)時,delete pA 的動作應(yīng)轉(zhuǎn)化為以下動作:
上全局鎖
全局變量(DELETE_FILE,DELETE_LINE)賦值為文件名和行號2
delete operator A
調(diào)用~A()
上全局鎖
全局變量(DELETE_FILE,DELETE_LINE)賦值為文件名和行號1
delete operator B
調(diào)用~B()
返回~B()
調(diào)用operator delete B
記錄全局變量(DELETE_FILE,DELETE_LINE)值1并清除全局變量(DELETE_FILE,DELETE_LINE)值
打開全局鎖
返回operator delete B
返回delete operator B
返回~A()
調(diào)用 operator delete A
記錄全局變量(DELETE_FILE,DELETE_LINE)值1并清除全局變量(DELETE_FILE,DELETE_LINE)值
打開全局鎖
返回operator delete A
返回 delete operator A
嵌套刪除時對全局變量(DELETE_FILE,DELETE_LINE)現(xiàn)場保護的問題是指,上述步驟中在 A 的析構(gòu)函數(shù)中調(diào)用 delete m_pB 時,對全局變量(DELETE_FILE,DELETE_LINE)文件名和行號的賦值將覆蓋主程序中調(diào)用 delete pA 時對全局變量(DELETE_FILE,DELETE_LINE)的賦值,造成了在執(zhí)行 operator delete A 時,delete pA 的信息全部丟失。