一、前緒
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(size) MallocDebug(__FILE__, __LINE__, size) 3 #define FREE(p) FreeDebug(__FILE__, __LINE__, p) 4 #else 5 #define MALLOC(size) malloc(size) 6 #define FREE(p) free(p) 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++能實現自定義操作符,那么方案就完美了。