Windows使用復(fù)雜的內(nèi)存管理器控制和優(yōu)化內(nèi)存的使用(包括磁盤緩沖)。一旦內(nèi)存管理出現(xiàn)紕漏就會(huì)導(dǎo)致內(nèi)存泄漏。內(nèi)存泄漏的實(shí)質(zhì)一般是因?yàn)樵诙焉戏峙淞四硥K內(nèi)存但以后不再對(duì)其重新分配,使得該部分內(nèi)存失去重用性。出現(xiàn)這一問(wèn)題的多數(shù)應(yīng)用程序一開始往往正常運(yùn)行,所以要檢測(cè)出該類問(wèn)題是較為困難的。不過(guò),要將其找出并得到正確的處理才更麻煩。大多數(shù)MFC應(yīng)用程序允許Windows安全地管理分配給資源的內(nèi)存,如果分配內(nèi)存的組件不由系統(tǒng)所處理的話內(nèi)存泄漏的危險(xiǎn)就大大增加了。這里通過(guò)舉例來(lái)討論一些相關(guān)的問(wèn)題。
示例:多次重繪窗口導(dǎo)致內(nèi)存泄漏
我們簡(jiǎn)單建立一個(gè)STD的MFC工程MLeak,該程序首先創(chuàng)建邏輯字體,隨后TextOut() 函數(shù)在窗口的客戶區(qū)書寫文本,如果程序類似圖1(略)左那樣持續(xù)再長(zhǎng)時(shí)間你也看不到會(huì)出現(xiàn)什么奇怪的現(xiàn)象。但你用鼠標(biāo)抓住窗口的邊界改變窗口大小多次(多的時(shí)候要到數(shù)十次)就會(huì)看見窗口變成了圖1右那樣:字體出問(wèn)題了。TextOut()函數(shù)仍然可以在窗口上書寫文本,但是邏輯字體卻沒(méi)有得到正確的創(chuàng)建。一般會(huì)認(rèn)為問(wèn)題出在OnDraw()函數(shù)內(nèi)的字體創(chuàng)建過(guò)程中。真是這樣嗎?
查找和分析問(wèn)題
幸好有些MFC類和函數(shù)可以用于發(fā)現(xiàn)內(nèi)存泄漏。添加相應(yīng)代碼就有助于檢查CMLeakView類中存在的內(nèi)存泄漏問(wèn)題(關(guān)鍵的代碼以粗體標(biāo)識(shí))。首先我們用ClassWizard為視圖類加入 OnCreate() 函數(shù),目的是為了在程序初始化時(shí)獲得堆的有關(guān)統(tǒng)計(jì)數(shù)據(jù)。只要調(diào)用oldMemState.Checkpoint()函數(shù)即可做到這一點(diǎn)。接著OnDraw()函數(shù)內(nèi)在完成與字體有關(guān)的全部工作后將執(zhí)行以下附加的調(diào)試代碼:
#ifdef _DEBUG
newMemState.Checkpoint();
if(diffMemState.Difference
(oldMemState, newMemState))
{
TRACE("Difference between first and now!\n\n");
diffMemState.DumpStatistics();
}
#endif
調(diào)用newMemState.Checkpoint() 將獲得堆的最新情況,diffMemState.Difference()則在原始值和當(dāng)前值出現(xiàn)差異時(shí)返回信息。統(tǒng)計(jì)結(jié)果通過(guò)調(diào)用diffMemState.DumpStatistics()被扔出。因?yàn)樵撔畔贠nDraw()函數(shù)內(nèi),而OnDraw()函數(shù)響應(yīng)WM_PAINT消息重繪屏幕窗口,則在每次改變窗口大小時(shí)將打印出統(tǒng)計(jì)結(jié)果,我們發(fā)現(xiàn)每次公布的統(tǒng)計(jì)數(shù)據(jù)的最后一行才有變化:
Difference between first and now!
(第一次統(tǒng)計(jì)信息的開始行)
… …
Total allocations: 87 bytes.
(第一次統(tǒng)計(jì)信息的結(jié)束行)
……
Total allocations: 132 bytes.
(第二次統(tǒng)計(jì)信息的結(jié)束行)
… …
Total allocations: 14352 bytes.
(最后一次統(tǒng)計(jì)信息的結(jié)束行)
可以注意到每次重繪屏幕都導(dǎo)致整個(gè)分配區(qū)在增加,增加幅度為45字節(jié),重繪一定次數(shù)后內(nèi)存分配就到達(dá)了14,352字節(jié)。那么會(huì)不會(huì)是忘了為邏輯字體結(jié)構(gòu)分配內(nèi)存呢?我們?cè)傧騉nDraw()函數(shù)中插入以下粗體代碼:
LOGFONT lf;
… …
memset(&lf,0,sizeof(LOGFONT));
… …
結(jié)果如故,說(shuō)明邏輯字體結(jié)構(gòu)大小并沒(méi)有與此發(fā)生必然聯(lián)系。從OnDraw()函數(shù)中去掉字體創(chuàng)建過(guò)程并加入到OnCreate()中,使邏輯字體資源在創(chuàng)建窗口時(shí)得到創(chuàng)建,不過(guò)還是可以發(fā)現(xiàn)分配的整個(gè)內(nèi)存仍然持續(xù)增加!于是修改OnDraw()如下:
void CMLeakView::OnDraw(CDC* pDC)
{
CMLeakDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
pDC->TextOut(20, 200,
"This program has memory problems");
#ifdef _DEBUG
newMemState.Checkpoint();
if(diffMemState.Difference
(oldMemState, newMemState)) {
TRACE("Difference between first and now!\n\n");
diffMemState.DumpStatistics();
}
#endif
}
問(wèn)題仍然出現(xiàn),而以下代碼是OnDraw()中所增加的唯一代碼:
pDC- >TextOut
(20, 200, "This program has memory problems");
將該行代碼注釋掉并重新編譯、運(yùn)行診斷程序。可以發(fā)現(xiàn)整個(gè)內(nèi)存分配統(tǒng)計(jì)結(jié)果增幅為0。看來(lái),分配給字符串的內(nèi)存在屏幕每次重繪時(shí)被重新分配了。
內(nèi)存診斷參數(shù)
啟用或禁用內(nèi)存診斷可以調(diào)用全局函數(shù)AfxEnableMemoryTracking()。Debugger將自動(dòng)地控制它,所以該函數(shù)作為開關(guān)函數(shù)將顯著增加程序執(zhí)行速度并減少診斷信息。MFC全局變量afxMemDF則使得特定內(nèi)存診斷特性可用。該變量信息可以查閱相關(guān)資料。
查找內(nèi)存泄漏
我們首先實(shí)現(xiàn)一個(gè)CMemoryState對(duì)象(CMemoryState的使用可參看有關(guān)資料)。在輸入有問(wèn)題代碼之前調(diào)用Checkpoint()函數(shù) 來(lái)獲得內(nèi)存使用的原始情況。然后實(shí)現(xiàn)另一個(gè)CMemoryState對(duì)象并在寫完有問(wèn)題代碼之后調(diào)用Checkpoint()函數(shù)來(lái)得到內(nèi)存使用后的情況。當(dāng)然,還可以實(shí)現(xiàn)第三個(gè)CMemoryState對(duì)象并調(diào)用Difference()成員函數(shù)。調(diào)用該函數(shù)時(shí)用先前的兩個(gè)CMemoryState對(duì)象作為其參數(shù)。如果內(nèi)存前后沒(méi)有差異則函數(shù)返回值非0。這樣至少可以說(shuō)明是否某些內(nèi)存塊還沒(méi)有釋放。以下是使用這三個(gè)對(duì)象的部分代碼:
#ifdef _DEBUG
CMemoryState oldMemState,
newMemState, diffMemState;
oldMemState.Checkpoint();
#endif
…
(被測(cè)試的代碼)
…
#ifdef _DEBUG
newMemState.Checkpoint();
if(diffMemState.Difference
(oldMemState, newMemState))
{
TRACE("Memory Leaked Here:\n\n" );
}
#endif
內(nèi)存狀況統(tǒng)計(jì)
CMemoryState() 成員函數(shù)可用于得到當(dāng)前內(nèi)存的統(tǒng)計(jì)資料或者兩個(gè)內(nèi)存對(duì)象狀態(tài)的差異。此外還可用于查找堆上內(nèi)存泄漏。以下代碼使用了原始信息來(lái)檢測(cè)當(dāng)前的內(nèi)存狀態(tài):
TRACE("Current Memory Picture:\n\n" );
NewMemState.DumpStatistics();
很容易獲取先后內(nèi)存狀態(tài)的差異:
if( diffMemState.Difference
(oldMemState,newMemState))
{
TRACE( "Memory Leaked Here:\n\n");
diffMemState.DumpStatistics();}
diffMemState.DumpStatistics()的示例輸出如下:
0 bytes in 0 Free Blocks
2 bytes in 1 Object Blocks
50 bytes in 5 Non-Object Blocks
Largest number used: 76 bytes
Total allocations: 304 bytes
以上代碼第一行指示延遲釋放的內(nèi)存塊數(shù)目。當(dāng)afxMemDF 變量設(shè)置為delayFreeMemDF 時(shí)就會(huì)這樣。第二行用于指示多少對(duì)象還存在于堆上。第三行指示多少非對(duì)象塊(新分配的)被分配并且沒(méi)有被釋放。第四行指示應(yīng)用程序在給定時(shí)間內(nèi)使用的最大內(nèi)存。最后一行指示工程使用的全部?jī)?nèi)存。以上任何一行出現(xiàn)問(wèn)題都意味著內(nèi)存泄漏了。
修復(fù)工程
雖然在CMLeakView類中適當(dāng)處理OnDraw()中的字符串也可能成功解決先前問(wèn)題,不過(guò)AppWizard已經(jīng)創(chuàng)建了負(fù)責(zé)存儲(chǔ)和分配工作的專門類CMLeakDoc文檔類。 我們可以將要顯示的字符串在MLeakDoc.h文件中聲明為CMLeakDoc的成員變量:
CString myCString;
然后在在CMLeakDoc的構(gòu)造函數(shù)中對(duì)其賦值:
CMLeakDoc::CMLeakDoc()
{
myCString = "This program doesn‘t have a leak";
}
最后修復(fù)的工程文件大致如下所示:
// MLeakView.cpp :
implementation of the CMLeakView class
//
… …
CFont NFont;
… …
void CMLeakView::OnDraw(CDC* pDC)
{
… …
CFont* pOFont;
pOFont = pDC- >SelectObject(&NFont);
pDC- >TextOut(20, 200, pDoc- >myCString);
DeleteObject(pOFont);
}
… …
int CMLeakView::OnCreate
(LPCREATESTRUCT lpCreateStruct)
{
if (CView::OnCreate(lpCreateStruct) == -1)
return -1;
LOGFONT lf;
memset(&lf,0,sizeof(LOGFONT));
lf.lfHeight = 50;
lf.lfWeight=FW_NORMAL;
lf.lfEscapement=0;
lf.lfOrientation=0;
lf.lfItalic=false;
lf.lfUnderline = false;
lf.lfStrikeOut = false;
lf.lfCharSet=ANSI_CHARSET;
lf.lfPitchAndFamily=34; //Arial
NFont.CreateFontIndirect(&lf);
return 0;
}
以上的一些技術(shù)性的手段可以使程序員對(duì)一些很隱蔽的內(nèi)存陷阱有一些新的認(rèn)識(shí),不過(guò),發(fā)現(xiàn)并能解決內(nèi)存泄漏問(wèn)題始終是個(gè)需要耐心和細(xì)心的過(guò)程,經(jīng)驗(yàn)或許會(huì)更重于技術(shù)指南。