內存管理器控制

信息產業部數據所
廖 錚

---- Windows使用複雜的內存管理器控制和優化內存的使用(包括磁盤緩衝)。一旦內存管理出現紕漏就會導致內存泄漏。內存泄漏的實質一般是因爲在堆上分配了某塊內存但以後不再對其重新分配,使得該部分內存失去重用性。出現這一問題的多數應用程序一開始往往正常運行,所以要檢測出該類問題是較爲困難的。不過,要將其找出並得到正確的處理才更麻煩。大多數MFC應用程序允許Windows安全地管理分配給資源的內存,如果分配內存的組件不由系統所處理的話內存泄漏的危險就大大增加了。這裏通過舉例來討論一些相關的問題。

示例:多次重繪窗口導致內存泄漏

---- 我們簡單建立一個STD的MFC工程MLeak,該程序首先創建邏輯字體,隨後TextOut() 函數在窗口的客戶區書寫文本,如果程序類似圖1(略)左那樣持續再長時間你也看不到會出現什麼奇怪的現象。但你用鼠標抓住窗口的邊界改變窗口大小多次(多的時候要到數十次)就會看見窗口變成了圖1右那樣:字體出問題了。TextOut()函數仍然可以在窗口上書寫文本,但是邏輯字體卻沒有得到正確的創建。一般會認爲問題出在OnDraw()函數內的字體創建過程中。真是這樣嗎?

查找和分析問題

---- 幸好有些MFC類和函數可以用於發現內存泄漏。添加相應代碼就有助於檢查CMLeakView類中存在的內存泄漏問題(關鍵的代碼以粗體標識)。首先我們用ClassWizard爲視圖類加入 OnCreate() 函數,目的是爲了在程序初始化時獲得堆的有關統計數據。只要調用oldMemState.Checkpoint()函數即可做到這一點。接着OnDraw()函數內在完成與字體有關的全部工作後將執行以下附加的調試代碼:

#ifdef _DEBUG newMemState.Checkpoint(); if(diffMemState.Difference (oldMemState, newMemState)) { TRACE("Difference between first and now!/n/n"); diffMemState.DumpStatistics(); } #endif 
---- 調用newMemState.Checkpoint() 將獲得堆的最新情況,diffMemState.Difference()則在原始值和當前值出現差異時返回信息。統計結果通過調用diffMemState.DumpStatistics()被扔出。因爲該信息包含在OnDraw()函數內,而OnDraw()函數響應WM_PAINT消息重繪屏幕窗口,則在每次改變窗口大小時將打印出統計結果,我們發現每次公佈的統計數據的最後一行纔有變化:
Difference between first and now! (第一次統計信息的開始行) … … Total allocations: 87 bytes. (第一次統計信息的結束行) …… Total allocations: 132 bytes. (第二次統計信息的結束行) … … Total allocations: 14352 bytes. (最後一次統計信息的結束行) 
---- 可以注意到每次重繪屏幕都導致整個分配區在增加,增加幅度爲45字節,重繪一定次數後內存分配就到達了14,352字節。那麼會不會是忘了爲邏輯字體結構分配內存呢?我們再向OnDraw()函數中插入以下粗體代碼:
 LOGFONT lf; … … memset(&lf,0,sizeof(LOGFONT)); … … 
---- 結果如故,說明邏輯字體結構大小並沒有與此發生必然聯繫。從OnDraw()函數中去掉字體創建過程並加入到OnCreate()中,使邏輯字體資源在創建窗口時得到創建,不過還是可以發現分配的整個內存仍然持續增加!於是修改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"); 
---- 將該行代碼註釋掉並重新編譯、運行診斷程序。可以發現整個內存分配統計結果增幅爲0。看來,分配給字符串的內存在屏幕每次重繪時被重新分配了。

內存診斷參數

---- 啓用或禁用內存診斷可以調用全局函數AfxEnableMemoryTracking()。Debugger將自動地控制它,所以該函數作爲開關函數將顯著增加程序執行速度並減少診斷信息。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 

內存狀況統計

---- CMemoryState() 成員函數可用於得到當前內存的統計資料或者兩個內存對象狀態的差異。此外還可用於查找堆上內存泄漏。以下代碼使用了原始信息來檢測當前的內存狀態:

TRACE("Current Memory Picture:/n/n" ); NewMemState.DumpStatistics(); 
---- 很容易獲取先後內存狀態的差異:
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已經創建了負責存儲和分配工作的專門類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; } 
---- 以上的一些技術性的手段可以使程序員對一些很隱蔽的內存陷阱有一些新的認識,不過,發現並能解決內存泄漏問題始終是個需要耐心和細心的過程,經驗或許會更重於技術指南。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章