Memory Leak - 最快速度找到記憶體洩漏

Memory Leak

出處

[http://blog.csdn.net/xushiweizh/archive/2006/12/20/1451083.aspx]

Code

取代new

#ifdef _DEBUG
#define new   new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif

設定Flag

_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
_CrtSetBreakAlloc(##);

原文

最快速度找到記憶體洩漏

記憶體管理是C++程序員的痛。我的《記憶體管理變革》系列就是試圖討論更為有效的記憶體管理方式,以杜絕(或減少)記憶體洩漏,減輕C++程序員的負擔。由於工作忙的緣故,這個系列目前未完,暫停。
這篇短文我想換個方式,討論一下如何以最快的速度找到記憶體洩漏。

確認是否存在記憶體洩漏

我們知道,MFC程序如果檢測到存在記憶體洩漏,退出程序的時候會在調試窗口提醒記憶體洩漏。例如:

class CMyApp : public CWinApp
{
public:
   BOOL InitApplication()
   {
       int* leak = new int[10];
       return TRUE;
   }
};

產生的記憶體洩漏報告大體如下:
Detected memory leaks!
Dumping objects ->
c:\work\test.cpp(186) : {52} normal block at 0x003C4410, 40 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

這挺好。問題是,如果我們不喜歡MFC,那麼難道就沒有辦法?或者自己做?

在非MFC環境下的範例

呵呵,這不需要。其實,MFC也沒有自己做。記憶體洩漏檢測的工作是VC++的C運行庫做的。也就是說,只要你是VC++程序員,都可以很方便地檢測記憶體洩漏。我們還是給個樣例:

#include <crtdbg.h>

inline void EnableMemLeakCheck()
{
 _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}

void main()
{
   EnableMemLeakCheck();
   int* leak = new int[10];
}

運行(提醒:不要按Ctrl+F5,按F5),你將發現,產生的記憶體洩漏報告與MFC類似,但有細節不同,如下:
Detected memory leaks!
Dumping objects ->
{52} normal block at 0x003C4410, 40 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

為什麼呢?看下面。

定位記憶體洩漏由於哪一句話引起的

你已經發現程序存在記憶體洩漏。現在的問題是,我們要找洩漏的根源。
一般我們首先確定記憶體洩漏是由於哪一句引起。在MFC中,這一點很容易。你雙擊記憶體洩漏報告的文字,或者在Debug窗口中按F4,IDE就幫你定位到申請該記憶體塊的地方。
對於上例,也就是這一句:

   int* leak = new int[10];

這多多少少對你分析記憶體洩漏有點幫助。特別地,如果這個new僅對應一條delete(或者你把delete漏寫),這將很快可以確認問題的癥結。
我們前面已經看到,不使用MFC的時候,生成的記憶體洩漏報告與MFC不同,而且你立刻發現按F4不靈。那麼難道MFC做了什麼手腳?

模擬MFC所做的是

其實不是,我們來模擬下MFC做的事情。看下例:

inline void EnableMemLeakCheck()
{
 _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}

#ifdef _DEBUG
#define new   new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif

void main()
{
   EnableMemLeakCheck();
   int* leak = new int[10];
}

再運行這個樣例,你驚喜地發現,現在記憶體洩漏報告和MFC沒有任何分別了。

快速找到記憶體洩漏

單確定了記憶體洩漏發生在哪一行,有時候並不足夠。特別是同一個new對應有多處釋放的情形。在實際的工程中,以下兩種情況很典型:
創建對象的地方是一個類工廠(ClassFactory)模式。很多甚至全部類實例由同一個new創建。對於此,定位到了new出對象的所在行基本沒有多大幫助。
COM對象。我們知道COM對像採用Reference Count維護生命週期。也就是說,對像new的地方只有一個,但是Release的地方很多,你要一個個排除。那麼,有什麼好辦法,可以迅速定位記憶體洩漏?
答:有。
在記憶體洩漏情況複雜的時候,你可以用以下方法定位記憶體洩漏。這是我個人認為通用的記憶體洩漏追蹤方法中最有效的手段。
我們再回頭看看crtdbg生成的記憶體洩漏報告:

Detected memory leaks!
Dumping objects ->
c:\work\test.cpp(186) : {52} normal block at 0x003C4410, 40 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

除了產生該記憶體洩漏的記憶體分配語句所在的檔案名、行號為,我們注意到有一個比較陌生的信息:{52}。這個整數值代表了什麼意思呢?
其實,它代表了第幾次記憶體分配操作。像這個例子,{52}代表了第52次記憶體分配操作發生了洩漏。你可能要說,我只new過一次,怎麼會是第52次?這很容易理解,其他的記憶體申請操作在C的初始化過程調用的唄。:)
有沒有可能,我們讓程序運行到第52次記憶體分配操作的時候,自動停下來,進入調試狀態?所幸,crtdbg確實提供了這樣的函數:即
long _CrtSetBreakAlloc(long nAllocID)

。我們加上它:
inline void EnableMemLeakCheck()
{
 _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}

#ifdef _DEBUG
#define new   new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif

void main()
{
   EnableMemLeakCheck();
   _CrtSetBreakAlloc(52);
   int* leak = new int[10];
}

你發現,程序運行到 int* leak = new int[10]; 一句時,自動停下來進入調試狀態。細細體會一下,你可以發現,這種方式你獲得的信息遠比在程序退出時獲得檔案名及行號有價值得多。因為報告洩漏檔案名及行號,你獲得的只是靜態的信息,然而_CrtSetBreakAlloc則是把整個現場恢復,你可以通過對函數調用棧分析(我發現很多人不習慣看函數調用棧,如果你屬於這種情況,我強烈推薦你去補上這一課,因為它太重要了)以及其他線上調試技巧,來分析產生記憶體洩漏的原因。通常情況下,這種分析方法可以在5分鐘內找到肇事者。
當然,_CrtSetBreakAlloc要求你的程序執行過程是可還原的(多次執行過程的記憶體分配順序不會發生變化)。這個假設在多數情況下成立。不過,在多執行緒的情況下,這一點有時難以保證。
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-NonCommercial-NoDerivs 3.0 License