許式偉
2006年11月某日
記憶體管理是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也沒有自己做。記憶體洩漏檢測的工作是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做的事情。看下例:
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要求你的程式執行過程是可還原的(多次執行過程的記憶體配置設定順序不會發生變化)。這個假設在多數情況下成立。不過,在多線程的情況下,這一點有時難以保證。