處理記憶體洩漏的一種MFC方法 (轉)

worldblog發表於2007-12-02
處理記憶體洩漏的一種MFC方法 (轉)[@more@]

 

資訊產業部資料所
廖 錚

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

示例:多次重繪視窗導致記憶體洩漏

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

查詢和分析問題

---- 幸好有些MFC類和函式可以用於發現記憶體洩漏。新增相應程式碼就有助於檢查CMLeakView類中存在的記憶體洩漏問題(關鍵的程式碼以粗體標識)。首先我們用ClassWizard為檢視類加入 OnCreate() 函式,目的是為了在程式初始化時獲得堆的有關統計資料。只要oldMemState.Checkpoint()函式即可做到這一點。接著OnDraw()函式內在完成與字型有關的全部工作後將以下附加的程式碼:

#ifdef _DE newMemState.Checkpoint(); if(diffMemState.Difference (oldMemState, newMemState)) { TRACE("Difference between first and now!nn"); 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!nn"); 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:nn" ); } #endif


記憶體狀況統計

---- CMemoryState() 成員函式可用於得到當前記憶體的統計資料或者兩個記憶體物件狀態的差異。此外還可用於查詢堆上記憶體洩漏。以下程式碼使用了原始資訊來檢測當前的記憶體狀態:

TRACE("Current Memory Picture:nn" ); NewMemState.DumpStatistics();

---- 很容易獲取先後記憶體狀態的差異:

if( diffMemState.Difference (oldMemState,newMemState)) { TRACE( "Memory Leaked Here:nn"); diffMemState.DumpStatistics(); } diffMemState.DumpStatistics()的示例輸出如下: 0 bytes in 0 Free Blocks 2 bytes in 1 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- >Object(&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; }

---- 以上的一些技術性的手段可以使程式設計師對一些很隱蔽的記憶體陷阱有一些新的認識,不過,發現並能解決記憶體洩漏問題始終是個需要耐心和細心的過程,或許會更重於技術指南。 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-987283/,如需轉載,請註明出處,否則將追究法律責任。

相關文章