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