4.錯(cuò)誤方式刪除帶來的問題
以上我們已經(jīng)構(gòu)建了一個(gè)具備基本內(nèi)存泄漏檢測(cè)功能的子系統(tǒng),下面讓我們來看一下關(guān)于內(nèi)存泄漏方面的一些稍微高級(jí)一點(diǎn)的話題。
首先,在我們編制 c++ 應(yīng)用時(shí),有時(shí)需要在堆上創(chuàng)建單個(gè)對(duì)象,有時(shí)則需要?jiǎng)?chuàng)建對(duì)象的數(shù)組。關(guān)于 new 和 delete 原理的敘述我們可以知道,對(duì)于單個(gè)對(duì)象和對(duì)象數(shù)組來說,內(nèi)存分配和刪除的動(dòng)作是大不相同的,我們應(yīng)該總是正確的使用彼此搭配的 new 和 delete 形式。但是在某些情況下,我們很容易犯錯(cuò)誤,比如如下代碼:

class Test
{};
……
Test* pAry = new Test[10];//創(chuàng)建了一個(gè)擁有 10 個(gè) Test 對(duì)象的數(shù)組
Test* pObj = new Test;//創(chuàng)建了一個(gè)單對(duì)象
……
delete []pObj;//本應(yīng)使用單對(duì)象形式 delete pObj 進(jìn)行內(nèi)存釋放,卻錯(cuò)誤的使用了數(shù)
//組形式
delete pAry;//本應(yīng)使用數(shù)組形式 delete []pAry 進(jìn)行內(nèi)存釋放,卻錯(cuò)誤的使用了單對(duì)
//象的形式

不匹配的 new 和 delete 會(huì)導(dǎo)致什么問題呢?C++ 標(biāo)準(zhǔn)對(duì)此的解答是"未定義",就是說沒有人向你保證會(huì)發(fā)生什么,但是有一點(diǎn)可以肯定:大多不是好事情--在某些編譯器形成的代碼中,程序可能會(huì)崩潰,而另外一些編譯器形成的代碼中,程序運(yùn)行可能毫無問題,但是可能導(dǎo)致內(nèi)存泄漏。
既然知道形式不匹配的 new 和 delete 會(huì)帶來的問題,我們就需要對(duì)這種現(xiàn)象進(jìn)行毫不留情的揭露,畢竟我們重載了所有形式的內(nèi)存操作 operator new,operator new[],operator delete,operator delete[]。
我們首先想到的是,當(dāng)用戶調(diào)用特定方式(單對(duì)象或者數(shù)組方式)的 operator new 來分配內(nèi)存時(shí),我們可以在指向該內(nèi)存的指針相關(guān)的數(shù)據(jù)結(jié)構(gòu)中,增加一項(xiàng)用于描述其分配方式。當(dāng)用戶調(diào)用不同形式的 operator delete 的時(shí)候,我們?cè)?map 中找到與該指針相對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu),然后比較分配方式和釋放方式是否匹配,匹配則在 map 中正常刪除該數(shù)據(jù)結(jié)構(gòu),不匹配則將該數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)移到一個(gè)所謂 "ErrorDelete" 的 list 中,在程序最終退出的時(shí)候和內(nèi)存泄漏信息一起打印。
上面這種方法是最順理成章的,但是在實(shí)際應(yīng)用中效果卻不好。原因有兩個(gè),第一個(gè)原因我們上面已經(jīng)提到了:當(dāng) new 和 delete 形式不匹配時(shí),其結(jié)果"未定義"。如果我們運(yùn)氣實(shí)在太差--程序在執(zhí)行不匹配的 delete 時(shí)崩潰了,我們的全局對(duì)象(appMemory)中存儲(chǔ)的數(shù)據(jù)也將不復(fù)存在,不會(huì)打印出任何信息。第二個(gè)原因與編譯器相關(guān),前面提到過,當(dāng)編譯器處理自定義數(shù)據(jù)類型或者自定義數(shù)據(jù)類型數(shù)組的 new 和 delete 操作符的時(shí)候,通常使用編譯器相關(guān)的 cookie 技術(shù)。這種 cookie 技術(shù)在編譯器中可能的實(shí)現(xiàn)方式是:new operator 先計(jì)算容納所有對(duì)象所需的內(nèi)存大小,而后再加上它為記錄 cookie 所需要的內(nèi)存量,再將總?cè)萘總鹘ooperator new 進(jìn)行內(nèi)存分配。當(dāng) operator new 返回所需的內(nèi)存塊后,new operator 將在調(diào)用相應(yīng)次數(shù)的構(gòu)造函數(shù)初始化有效數(shù)據(jù)的同時(shí),記錄 cookie 信息。而后將指向有效數(shù)據(jù)的指針返回給用戶。也就是說我們重載的 operator new 所申請(qǐng)到并記錄下來的指針與 new operator 返回給調(diào)用者的指針不一定一致(圖3)。當(dāng)調(diào)用者將 new operator 返回的指針傳給 delete operator 進(jìn)行內(nèi)存釋放時(shí),如果其調(diào)用形式相匹配,則相應(yīng)形式的 delete operator 會(huì)作出相反的處理,即調(diào)用相應(yīng)次數(shù)的析構(gòu)函數(shù),再通過指向有效數(shù)據(jù)的指針位置找出包含 cookie 的整塊內(nèi)存地址,并將其傳給 operator delete 釋放內(nèi)存。如果調(diào)用形式不匹配,delete operator 就不會(huì)做上述運(yùn)算,而直接將指向有效數(shù)據(jù)的指針(而不是真正指向整塊內(nèi)存的指針)傳入 operator delete。因?yàn)槲覀冊(cè)?operator new 中記錄的是我們所分配的整塊內(nèi)存的指針,而現(xiàn)在傳入 operator delete 的卻不是,所以就無法在全局對(duì)象(appMemory)所記錄的數(shù)據(jù)中找到相應(yīng)的內(nèi)存分配信息。
圖3
綜上所述,當(dāng) new 和 delete 的調(diào)用形式不匹配時(shí),由于程序有可能崩潰或者內(nèi)存子系統(tǒng)找不到相應(yīng)的內(nèi)存分配信息,在程序最終打印出 "ErrorDelete" 的方式只能檢測(cè)到某些"幸運(yùn)"的不匹配現(xiàn)象。但我們總得做點(diǎn)兒什么,不能讓這種危害極大的錯(cuò)誤從我們眼前溜走,既然不能秋后算帳,我們就實(shí)時(shí)輸出一個(gè) warning 信息來提醒用戶。什么時(shí)候拋出一個(gè) warning 呢?很簡(jiǎn)單,當(dāng)我們發(fā)現(xiàn)在 operator delete 或 operator delete[] 被調(diào)用的時(shí)候,我們無法在全局對(duì)象(appMemory)的 map 中找到與傳入的指針值相對(duì)應(yīng)的內(nèi)存分配信息,我們就認(rèn)為應(yīng)該提醒用戶。
既然決定要輸出warning信息,那么現(xiàn)在的問題就是:我們?nèi)绾蚊枋鑫覀兊膚arning信息才能更便于用戶定位到不匹配刪除錯(cuò)誤呢?答案:在 warning 信息中打印本次 delete 調(diào)用的文件名和行號(hào)信息。這可有點(diǎn)困難了,因?yàn)閷?duì)于 operator delete 我們不能向?qū)ο?operator new 一樣做出一個(gè)帶附加信息的重載版本,我們只能在保持其接口原貌的情況下,重新定義其實(shí)現(xiàn),所以我們的 operator delete 中能夠得到的輸入只有指針值。在 new/delete 調(diào)用形式不匹配的情況下,我們很有可能無法在全局對(duì)象(appMemory)的 map 中找到原來的 new 調(diào)用的分配信息。怎么辦呢?萬不得已,只好使用全局變量了。我們?cè)跈z測(cè)子系統(tǒng)的實(shí)現(xiàn)文件中定義了兩個(gè)全局變量(DELETE_FILE,DELETE_LINE)記錄 operator delete 被調(diào)用時(shí)的文件名和行號(hào),同時(shí)為了保證并發(fā)的 delete 操作對(duì)這兩個(gè)變量訪問同步,還使用了一個(gè) mutex(至于為什么是 CCommonMutex 而不是一個(gè) pthread_mutex_t,在"實(shí)現(xiàn)上的問題"一節(jié)會(huì)詳細(xì)論述,在這里它的作用就是一個(gè) mutex)。

char DELETE_FILE[ FILENAME_LENGTH ] =
{0};
int DELETE_LINE = 0;
CCommonMutex globalLock;
而后,在我們的檢測(cè)子系統(tǒng)的頭文件中定義了如下形式的 DEBUG_DELETE
extern char DELETE_FILE[ FILENAME_LENGTH ];
extern int DELETE_LINE;
extern CCommonMutex globalLock;//在后面解釋
#define DEBUG_DELETE globalLock.Lock(); \
if (DELETE_LINE != 0) BuildStack();\ (//見第六節(jié)解釋)
strncpy( DELETE_FILE, __FILE__,FILENAME_LENGTH - 1 );\
DELETE_FILE[ FILENAME_LENGTH - 1 ]= '\0'; \
DELETE_LINE = __LINE__; \
delete
在用戶被檢測(cè)文件中原來的宏定義中添加一條:
#include "MemRecord.h"
#if defined( MEM_DEBUG )
#define new DEBUG_NEW
#define delete DEBUG_DELETE
#endif

這樣,在用戶被檢測(cè)文件調(diào)用 delete operator 之前,將先獲得互斥鎖,然后使用調(diào)用點(diǎn)文件名和行號(hào)對(duì)相應(yīng)的全局變量(DELETE_FILE,DELETE_LINE)進(jìn)行賦值,而后調(diào)用 delete operator。當(dāng) delete operator 最終調(diào)用我們定義的 operator delete 的時(shí)候,在獲得此次調(diào)用的文件名和行號(hào)信息后,對(duì)文件名和行號(hào)全局變量(DELETE_FILE,DELETE_LINE)重新初始化并打開互斥鎖,讓下一個(gè)掛在互斥鎖上的 delete operator 得以執(zhí)行。
在對(duì) delete operator 作出如上修改以后,當(dāng)我們發(fā)現(xiàn)無法經(jīng)由 delete operator 傳入的指針找到對(duì)應(yīng)的內(nèi)存分配信息的時(shí)候,就打印包括該次調(diào)用的文件名和行號(hào)的 warning。
天下沒有十全十美的事情,既然我們提供了一種針對(duì)錯(cuò)誤方式刪除的提醒方法,我們就需要考慮以下幾種異常情況:
1. 用戶使用的第三方庫(kù)函數(shù)中有內(nèi)存分配和釋放操作。或者用戶的被檢測(cè)進(jìn)程中進(jìn)行內(nèi)存分配和釋放的實(shí)現(xiàn)文件沒有使用我們的宏定義。 由于我們替換了全局的 operator delete,這種情況下的用戶調(diào)用的 delete 也會(huì)被我們截獲。用戶并沒有使用我們定義的DEBUG_NEW 宏,所以我們無法在我們的全局對(duì)象(appMemory)數(shù)據(jù)結(jié)構(gòu)中找到對(duì)應(yīng)的內(nèi)存分配信息,但是由于它也沒有使用DEBUG_DELETE,我們?yōu)?delete 定義的兩個(gè)全局 DELETE_FILE 和 DELETE_LINE 都不會(huì)有值,因此可以不打印 warning。
2. 用戶的一個(gè)實(shí)現(xiàn)文件調(diào)用了 new 進(jìn)行內(nèi)存分配工作,但是該文件并沒有使用我們定義的 DEBUG_NEW 宏。同時(shí)用戶的另一個(gè)實(shí)現(xiàn)文件中的代碼負(fù)責(zé)調(diào)用 delete 來刪除前者分配的內(nèi)存,但不巧的是,這個(gè)文件使用了 DEBUG_DELETE 宏。這種情況下內(nèi)存檢測(cè)子系統(tǒng)會(huì)報(bào)告 warning,并打印出 delete 調(diào)用的文件名和行號(hào)。
3. 與第二種情況相反,用戶的一個(gè)實(shí)現(xiàn)文件調(diào)用了 new 進(jìn)行內(nèi)存分配工作,并使用我們定義的 DEBUG_NEW 宏。同時(shí)用戶的另一個(gè)實(shí)現(xiàn)文件中的代碼負(fù)責(zé)調(diào)用 delete 來刪除前者分配的內(nèi)存,但該文件沒有使用 DEBUG_DELETE 宏。這種情況下,因?yàn)槲覀兡軌蛘业竭@個(gè)內(nèi)存分配的原始信息,所以不會(huì)打印 warning。
4. 當(dāng)出現(xiàn)嵌套 delete(定義可見"實(shí)現(xiàn)上的問題")的情況下,以上第一和第三種情況都有可能打印出不正確的 warning 信息,詳細(xì)分析可見"實(shí)現(xiàn)上的問題"一節(jié)。
你可能覺得這樣的 warning 太隨意了,有誤導(dǎo)之嫌。怎么說呢?作為一個(gè)檢測(cè)子系統(tǒng),對(duì)待有可能的錯(cuò)誤我們所采取的原則是:寧可誤報(bào),不可漏報(bào)。請(qǐng)大家"有則改之,無則加勉"。