VC++內存泄漏的檢測與定位

VC++內存泄漏的檢測與定位

本文大部分內容來自網絡,只是做了適當的修改和補充,以便更貼近實際應用。

一 對於MFC程序
如果檢測到存在內存泄漏,退出程序的時候會在調試窗口提醒內存泄漏。例如:
class CMyApp : public CWinApp
{
public:
BOOL InitApplication()
{
int* leak = new int[10];
return TRUE;
}
};

產生的內存泄漏報告大體如下:
Detected memory leaks!
Dumping objects ->
c:worktest.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.

雙擊“輸出”窗口中,內存泄漏報告的文字"c:worktest.cpp(186) : {52} normal block at 0x003C4410, 40 bytes long.",或者在Debug窗口中邏輯按F4(VC++6.0),IDE就幫你定位到引起內存泄漏的對應文件的對應行,也就是這一句:
int* leak = new int[10];
特別地,如果這個new僅對應一條delete(或者你把delete漏寫),這將很快可以確認問題的癥結。



二 對於非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做的事情。看下例:

#include "crtdbg.h"
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:worktest.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次內存分配操作發生了泄漏。你可能要說,我只new過一次,怎麼會是第52次?這很容易理解,其他的內存申請操作在C的初始化過程調用的唄。:)
有沒有可能,我們讓程序運行到第52次內存分配操作的時候,自動停下來,進入調試狀態?所幸,crtdbg確實提供了這樣的函數:即 long _CrtSetBreakAlloc(long nAllocID)。我們加上它:

#include "crtdbg.h"
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要求你的程序執行過程是可還原的(多次執行過程的內存分配順序不會發生變化)。這個假設在多數情況下成立。不過,在多線程的情況下,這一點有時難以保證。

四 補充說明

對應非MFC需要加入如下若干語句:

#include "crtdbg.h"
inline void EnableMemLeakCheck()
{
_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);
}
#ifdef _DEBUG
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif



使用時,要先註釋掉_CrtSetBreakAlloc語句,運行找到出錯的內存序號後,再打開該語句。對於 _CrtSetBreakAlloc(long (52))函數。其中52是申請內存的序號(上面有說明),然後按F5運行(不需要設斷點)。

在“VC++6.0”調試環境

當按F5運行出現“Find Source”這個對話框,點擊“Cancel取消”。 出現“User breakpoint called from code at xxxx”的對話框,點擊“確定”,會看到一些彙編的代碼,調出堆棧窗口(call stack),在其中的“main() line xxx + xxx bytes”上雙擊(或它的上一行雙擊,我的上一行是一個自定義函數,雙擊後直接定位到new的地方。當然,對於多個函數的,多雙擊有自己內容的函數,就會定位到出錯的地方。

在VS2010環境下的VC2010方式

當按F5運行出現 “Microsoft Visual Studio xxxx.exe 已觸發了一個斷點,中斷,繼續,忽略”提示框時,點擊其中的“中斷”按鈕,然後連續按鍵“Shift+F11”(就是菜單上的 調試--跳出),就會返回到自己程序出錯的語句上

3 這種情況我採用一種“試探法”。由於內存分配的塊號不是固定不變的,而是每次運行都是變化的,所以跟蹤起來很麻煩。但是我發現雖然內存分配的塊號是變化的,但是變化的塊號卻總是那幾個,也就是說多運行幾次,內存分配的塊號很可能會重複。因此這就是“試探法”的基礎。

3.1 先在調試狀態下運行幾次程序,觀察內存分配的塊號是哪幾個值;

3.2 選擇出現次數最多的塊號來設斷點,在代碼中設置內存分配斷點:

添加如下一行(對於第 1875 個內存分配):

_crtBreakAlloc = 1875;

或者,可以使用具有同樣效果的 _CrtSetBreakAlloc 函數:

_CrtSetBreakAlloc(1875); //我把這句放在了InitiaDialog裏面

3.3 在調試狀態下運行序,在斷點停下時,打開“調用堆棧”窗口,找到對應的源代碼處;

3.4 退出程序,觀察“輸出窗口”的內存泄露報告,看實際內存分配的塊號是不是和預設值相同,如果相同,就找到了;如果不同,就重複步驟3,直到相同。

3.5 最後就是根據具體情況,在適當的位置釋放所分配的內存。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章