C/C++應用程式記憶體洩漏檢查統計方案

松柏玫瑰發表於2019-07-06

  一、前緒

  C/C++程式給某些程式設計師的幾大印象之一就是記憶體自己管理容易洩漏容易崩,筆者曾經在一個產品中使用C語言開發維護部分模組,只要產品有記憶體洩漏和崩潰的問題,就被甩鍋“我的程式是C#開發的記憶體都是託管的,C++那邊也沒有記憶體(庇護其好友),肯定是C這邊的問題”(話說一個十幾年的程式設計師還停留在語言層面不覺得有點low嗎),筆者畢業不到一年,聽到此語心裡一萬頭草泥馬奔騰而過,默默地修改了程式,注意不是修改bug(哈哈),而是把所有malloc和free都替換成了自定義巨集MALLOC和FREE,debug版本所有記憶體分配釋放都打了日誌,程式結束自動報告類似“Core Memory Leaks: 位元組數”,此後記憶體洩漏的問題再也沒人敢甩過來了。語言僅僅是個工具,人心是大道。

  二、C程式記憶體洩漏檢測方案參考

  C語言應用程式一般使用malloc和free兩個函式分配和釋放記憶體,對它們做記憶體洩漏檢測還是很好想到完美方案的。所謂的完美:1)當記憶體洩漏時能迅速定位到是哪一行程式碼分配的;2)使用簡單與原先無異;3)release時或者不需要除錯記憶體的時候,仍然使用原生態函式,不影響效率。

 1 #ifdef DEBUG_MEMORY
 2 #define MALLOC MallocDebug(__FILE__, __LINE__, size)
 3 #define FREE FreeDebug(__FILE__, __LINE__, p)
 4 #else
 5 #define MALLOC malloc
 6 #define FREE free
 7 #endif
 8 
 9 #ifdef DEBUG_MEMORY
10 #define MEM_OP_MALLOC 1
11 #define MEM_OP_FREE 0
12 
13 void LogMemory(const char* file, int line, void* p, int operation, size_t size);
14 
15 void* MallocDebug(const char* file, int line, size_t size)
16 {
17     void* p = malloc(size);
18     LogMemory(file, line, p, MEM_OP_MALLOC, size);
19     return p;
20 }
21 
22 void FreeDebug(const char* file, int line, void* p)
23 {
24     LogMemory(file, line, p, MEM_OP_FREE, 0);
25      free(p);     
26 }    
27 
28 void LogMemory(const char* file, int line, void* p, int operation, size_t size)
29 {
30     //列印日誌(malloc/free、指標、檔名、行號、指標、第幾次分配的序號),分配序號可以實現類似與crtdbg的CrtSetBreakAlloc函式的功能
31     //操作為malloc時,向map插入一條記錄,增加記憶體使用大小;
32     //操作為free時,在map中找到記錄並刪除,減少記憶體使用大小。
33 }
34 
35 void DetectMemoryLeaks()
36 {
37     //列印當前記憶體管理的map中剩餘的沒有釋放的記憶體指標、檔名、行號、大小、分配序號
38 }
39 
40 #endif
41 
42 void Program()
43 {
44     int *pArray = MALLOC(sizeof(int) * 10);
45     FREE(pArray);
46 
47 #ifdef DEBUG_MEMORY
48     DetectMemoryLeaks();
49 #endif
50 }

    C語言應用程式中的上述記憶體洩漏檢測方案至此完美收官,記錄分配序號,也可以向CrtSetBreakAlloc那樣除錯記憶體洩漏哦。

    三、C++程式記憶體洩漏檢測方案參考

    近期在跟蹤C++專案的記憶體洩漏,專案包含多個工程(1個exe+多個自開發dll+多個第三方dll)。

    1.首先考慮的第一個方案是利用crtdbg。踩得第一個坑是記得看下工程配置執行時庫選項用debug版本(/MTd或/MDd),否則無效。非MFC程式報不出可疑洩漏記憶體的檔名及行號,要在整個程式所有使用new的檔案中包含"#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)"的巨集定義。對於單個工程程式而言除錯比較簡單方便;對於多個dll尤其是有第三方庫時,/MTd配置下要非常小心,/MDd配置要好很多,但實際中使用crtdbg除錯還是偶爾會崩在系統底層記憶體分配的地方,出現的問題不在個人解決能力之內,放棄了。

    2.其次的第二個方案,考慮自己過載operator new和operator delete,當然是要過載operator new(size_t size, const char* file, int line)這個版本才能在洩漏時定位到行號。同樣也是要所有使用new的檔案中包含"#define new new( __FILE__, __LINE__)"的巨集定義。問題是雖然可以過載operator delete(void* p,  const char* file, int line)這個版本,但是這個版本只會在placement new失敗時才會呼叫,正常時候還是呼叫的operator delete(void* p)版本,所以還需要過載operator delete(void* p)版本,問題是沒有過載的系統內建的operator new(size_t size)版本分配的所有記憶體也會走使用者過載後的operator delete(void* p)版本,不配對,一起把operator new(size_t size)也過載了。    

      第二個方案的另外一個問題是程式要包含巨集"#define new new( __FILE__, __LINE__)",但第三方庫標頭檔案中有placement new的用法new(pointer)classA(),專案大一點標頭檔案順序不好調,編譯失敗。還有就是這個方案實踐中(多dll全部設定的相同的執行時庫配置)也在系統底層分配記憶體的方法崩潰過,也可能是個人在哪裡的處理有問題,總之不再考慮前兩個方案了,打算在應用層做處理。

    3.最後確定在最上層想方案,首先C++不能自定義操作符,否則就能定義一個操作符A* pA = debugnew A(1, 2)了。巨集不能有空格只能考慮函式debugnew(A, 1, 2)了。下面上方案。

    所有要分配或釋放記憶體的檔案中包含DebugMemory.h標頭檔案(虛擬碼):

 1 //檔名:DebugMemory.h
 2 
 3 #ifdef DEBUG_MEMORY
 4 #define NEW(T, ...) DebugNew<T>(__FILE__, __LINE__, __VA_ARGS__)
 5 #define DEL(p) DebugDelete(__FILE__, __LINE__, p)
 6 #define NEW_ARRAY(T, size) DebugNewArray<T>(__FILE__, __LINE__, size)
 7 #define DEL_ARRAY(p) DebugDeleteArray(__FILE__, __LINE__, p)
 8 #else
 9 #define NEW(T, ...) new T(__VA_ARGS__)
10 #define DEL(p) delete(p)
11 #define NEW_ARRAY(T, size) new T[size]
12 #define DEL_ARRAY(p) delete[] p
13 #endif
14 
15 #ifdef DEBUG_MEMORY
16 
17 template<class T, class... Args>
18 T* DebugNew(const char* file, int line, Args&&... args)
19 {
20     T* p = new T(std::forward<Args>(args)...);
21     //todo:記錄操作(new)、指標、檔案、行號、分配號
22     return p;
23 }
24 
25 template<class T>
26 void DebugDelete(const char* file, int line, T* p)
27 {
28     //todo:記錄操作(delete)、指標、檔案、行號
29     delete p;
30 }
31 
32 template<class T>
33 T* DebugNewArray(const char* file, int line, size_t size)
34 {
35     T* p = new T[size];
36     //todo:記錄操作(new[])、指標、檔案、行號、分配號
37     return p;
38 }
39 
40 template<class T>
41 void DebugDeleteArray(const char* file, int line, T* p)
42 {
43     //todo:記錄操作(delete)、指標、檔案、行號        
44     delete[] p;
45 }
46 
47 void DetectMemoryLeaks()
48 {
49     //todo:統計並列印未釋放的記憶體資訊
50 }
51 
52 #endif

    使用DebugMemory.h標頭檔案:

 1 //檔名:main.cpp
 2 
 3 #include "DebugMemory.h"
 4 
 5 class A
 6 {
 7 public:
 8     A(){}
 9     A(int a, int b):m_a(a), m_b(b){}
10 private:
11     int m_a;
12     int m_b;
13 }
14 
15 int main()
16 {
17     A* pA = NEW(A, 1, 2);         //new A(1, 2)
18     DEL(pA);                      //delete pA;
19 
20     A* pArray = NEW_ARRAY(A, 10); //new A[10]
21     DEL_ARRAY(pArray);            //delete[] pArray
22 
23 #ifdef DEBUG_MEMORY
24     DetectMemoryLeaks();          //記憶體洩漏檢測
25 #endif
26 
27     return 0;
28 }

    四、方案評價

1.C語言應用程式的記憶體洩漏解決方案:完美。

2.C++語言應用程式的記憶體洩漏解決方案

           優點:沒有改變預設的operator new和operator delete行為,畢竟危險。

           優點:實用性通用性強,完全在應用程式設計師的控制範圍內。因為在應用層,不管什麼版本都可以檢測記憶體洩漏,不用考慮跨dll呼叫產生的問題。

           不足:寫法習慣改變,原來是new A(1,2),要寫成NEW(A, 1, 2),如果C++能實現自定義操作符,那麼方案就完美了。

 

相關文章