要想檢測記憶體洩漏,就必須對程式中的記憶體分配和釋放情況進行記錄,所能夠採取的辦法就是過載所有形式的operator new 和 operator delete,截獲 new operator 和 delete operator 執行過程中的記憶體操作資訊。下面列出的就是過載形式
void* operator new( size_t nSize, char* pszFileName, int nLineNum )
void* operator new[]( size_t nSize, char* pszFileName, int nLineNum )
void operator delete( void *ptr )
void operator delete[]( void *ptr )
我們為 operator new 定義了一個新的版本,除了必須的 size_t nSize 引數外,還增加了檔名和行號,這裡的檔名和行號就是這次 new operator 運算子被呼叫時所在的檔名和行號,這個資訊將在發現記憶體洩漏時輸出,以幫助使用者定位洩漏具體位置。對於 operator delete,因為無法為之定義新的版本,我們直接覆蓋了全域性的 operator delete 的兩個版本。
在過載的 operator new 函式版本中,我們將呼叫全域性的 operator new 的相應的版本並將相應的 size_t 引數傳入,而後,我們將全域性 operator new 返回的指標值以及該次分配所在的檔名和行號資訊記錄下來,這裡所採用的資料結構是一個 STL 的 map,以指標值為 key 值。當 operator delete 被呼叫時,如果呼叫方式正確的話(呼叫方式不正確的情況將在後面詳細描述),我們就能以傳入的指標值在 map 中找到相應的資料項並將之刪除,而後呼叫 free 將指標所指向的記憶體塊釋放。當程式退出的時候,map 中的剩餘的資料項就是我們企圖檢測的記憶體洩漏資訊--已經在堆上分配但是尚未釋放的分配資訊。
以上就是記憶體檢測實現的基本原理,現在還有兩個基本問題沒有解決:
1) 如何取得記憶體分配程式碼所在的檔名和行號,並讓 new operator 將之傳遞給我們過載的 operator new。
2) 我們何時建立用於儲存記憶體資料的 map 資料結構,如何管理,何時列印記憶體洩漏資訊。
先解決問題1。首先我們可以利用 C 的預編譯宏 __FILE__ 和 __LINE__,這兩個宏將在編譯時在指定位置展開為該檔案的檔名和該行的行號。而後我們需要將預設的全域性 new operator 替換為我們自定義的能夠傳入檔名和行號的版本,我們在子系統標頭檔案 MemRecord.h 中定義:
#define DEBUG_NEW new(__FILE__, __LINE__ )
而後在所有需要使用記憶體檢測的客戶程式的所有的 cpp 檔案的開頭加入
#include "MemRecord.h"
#define new DEBUG_NEW
就可以將客戶原始檔中的對於全域性預設的 new operator 的呼叫替換為 new (__FILE__,__LINE__) 呼叫,而該形式的new operator將呼叫我們的operator new (size_t nSize, char* pszFileName, int nLineNum),其中 nSize 是由 new operator 計算並傳入的,而 new 呼叫點的檔名和行號是由我們自定義版本的 new operator 傳入的。我們建議在所有使用者自己的原始碼檔案中都加入上述宏,如果有的檔案中使用記憶體檢測子系統而有的沒有,則子系統將可能因無法監控整個系統而輸出一些洩漏警告。
要想檢測記憶體洩漏,就必須對程式中的記憶體分配和釋放情況進行記錄,所能夠採取的辦法就是過載所有形式的operator new 和 operator delete,截獲 new operator 和 delete operator 執行過程中的記憶體操作資訊。下面列出的就是過載形式
void* operator new( size_t nSize, char* pszFileName, int nLineNum )
void* operator new[]( size_t nSize, char* pszFileName, int nLineNum )
void operator delete( void *ptr )
void operator delete[]( void *ptr )
我們為 operator new 定義了一個新的版本,除了必須的 size_t nSize 引數外,還增加了檔名和行號,這裡的檔名和行號就是這次 new operator 運算子被呼叫時所在的檔名和行號,這個資訊將在發現記憶體洩漏時輸出,以幫助使用者定位洩漏具體位置。對於 operator delete,因為無法為之定義新的版本,我們直接覆蓋了全域性的 operator delete 的兩個版本。
在過載的 operator new 函式版本中,我們將呼叫全域性的 operator new 的相應的版本並將相應的 size_t 引數傳入,而後,我們將全域性 operator new 返回的指標值以及該次分配所在的檔名和行號資訊記錄下來,這裡所採用的資料結構是一個 STL 的 map,以指標值為 key 值。當 operator delete 被呼叫時,如果呼叫方式正確的話(呼叫方式不正確的情況將在後面詳細描述),我們就能以傳入的指標值在 map 中找到相應的資料項並將之刪除,而後呼叫 free 將指標所指向的記憶體塊釋放。當程式退出的時候,map 中的剩餘的資料項就是我們企圖檢測的記憶體洩漏資訊--已經在堆上分配但是尚未釋放的分配資訊。
以上就是記憶體檢測實現的基本原理,現在還有兩個基本問題沒有解決:
1) 如何取得記憶體分配程式碼所在的檔名和行號,並讓 new operator 將之傳遞給我們過載的 operator new。
2) 我們何時建立用於儲存記憶體資料的 map 資料結構,如何管理,何時列印記憶體洩漏資訊。
先解決問題1。首先我們可以利用 C 的預編譯宏 __FILE__ 和 __LINE__,這兩個宏將在編譯時在指定位置展開為該檔案的檔名和該行的行號。而後我們需要將預設的全域性 new operator 替換為我們自定義的能夠傳入檔名和行號的版本,我們在子系統標頭檔案 MemRecord.h 中定義:
#define DEBUG_NEW new(__FILE__, __LINE__ )
而後在所有需要使用記憶體檢測的客戶程式的所有的 cpp 檔案的開頭加入
#include "MemRecord.h"
#define new DEBUG_NEW
就可以將客戶原始檔中的對於全域性預設的 new operator 的呼叫替換為 new (__FILE__,__LINE__) 呼叫,而該形式的new operator將呼叫我們的operator new (size_t nSize, char* pszFileName, int nLineNum),其中 nSize 是由 new operator 計算並傳入的,而 new 呼叫點的檔名和行號是由我們自定義版本的 new operator 傳入的。我們建議在所有使用者自己的原始碼檔案中都加入上述宏,如果有的檔案中使用記憶體檢測子系統而有的沒有,則子系統將可能因無法監控整個系統而輸出一些洩漏警告。
再說第二個問題。我們用於管理客戶資訊的這個 map 必須在客戶程式第一次呼叫 new operator 或者 delete operator 之前被建立,而且在最後一個 new operator 和 delete operator 呼叫之後進行洩漏資訊的列印,也就是說它需要先於客戶程式而出生,而在客戶程式退出之後進行分析。能夠包容客戶程式生命週期的確有一人--全域性物件(appMemory)。我們可以設計一個類來封裝這個 map 以及這對它的插入刪除操作,然後構造這個類的一個全域性物件(appMemory),在全域性物件(appMemory)的建構函式中建立並初始化這個資料結構,而在其解構函式中對資料結構中剩餘資料進行分析和輸出。Operator new 中將呼叫這個全域性物件(appMemory)的 insert 介面將指標、檔名、行號、記憶體塊大小等資訊以指標值為 key 記錄到 map 中,在 operator delete 中呼叫 erase 介面將對應指標值的 map 中的資料項刪除,注意不要忘了對 map 的訪問需要進行互斥同步,因為同一時間可能會有多個執行緒進行堆上的記憶體操作。
好啦,記憶體檢測的基本功能已經具備了。但是不要忘了,我們為了檢測記憶體洩漏,在全域性的 operator new 增加了一層間接性,同時為了保證對資料結構的安全訪問增加了互斥,這些都會降低程式執行的效率。因此我們需要讓使用者能夠方便的 enable 和 disable 這個記憶體檢測功能,畢竟記憶體洩漏的檢測應該在程式的除錯和測試階段完成。我們可以使用條件編譯的特性,在使用者被檢測檔案中使用如下宏定義:
#include "MemRecord.h"
#if defined( MEM_DEBUG )
#define new DEBUG_NEW
#endif
當用戶需要使用記憶體檢測時,可以使用如下命令對被檢測檔案進行編譯
g++ -c -DMEM_DEBUG xxxxxx.cpp
就可以 enable 記憶體檢測功能,而使用者程式正式釋出時,可以去掉 -DMEM_DEBUG 編譯開關來 disable 記憶體檢測功能,消除記憶體檢測帶來的效率影響。