溫輝敏(http://blog.csdn.net/wenhm/article/details/mailto:wenhm@sina.com) 2009-7-1 晚摘要:本文簡單介紹了C++編程時,大家經(jīng)常犯得一些內(nèi)存泄漏方面的編碼錯誤,并給出簡單的代碼示例。并簡要給出了Win32平臺下使用檢測內(nèi)存泄漏利器DevPartner BoundsChecker進(jìn)行檢查以發(fā)現(xiàn)泄漏代碼的詳細(xì)步驟。值此黨的節(jié)日,希望對一些迷失在內(nèi)存泄漏中的同志們有所幫助避免少走彎路。我一直覺得黨的黨章是完美的,原則是好的,共產(chǎn)主義社會肯定比資本主義財富集中在少數(shù)人手里強(qiáng),只是到了下面執(zhí)行就有所欠缺了,這次上海閔行封頂房的倒塌正是沒有一個良好監(jiān)督機(jī)制的問題,官員參股房地產(chǎn)明顯違背政府、法律,希望祖國越來越美好。
閑話少說,切入正題,本人在參與一個大型Win32軟件項目時,對整個項目進(jìn)行了內(nèi)存泄漏方面的檢查,隨著泄漏代碼的一個個發(fā)現(xiàn),發(fā)現(xiàn)許多的泄漏都具有某些共同性,于是乎總結(jié)了一些常見泄漏代碼,發(fā)給同事們看了。希望能提醒下大家,但后來在項目的二期、三期版本出現(xiàn)的泄漏發(fā)現(xiàn)和以前的一些問題幾乎是同一性質(zhì)的,回過來看以前寫的一些筆記發(fā)現(xiàn)都概括了(當(dāng)時覺得自己是有那么兩把刷子,^_^)。于是想到也許很多編程同志也會碰到同樣的問題,能寫下來放到網(wǎng)上起到拋磚引玉的作用就更好了。
這里總結(jié)下檢測出來的內(nèi)存泄漏有共性的問題,希望對大家以后編程避免內(nèi)存泄漏有所幫助: 1. 類內(nèi)成員動態(tài)分配類所有動態(tài)分配的成員變量,一定記得在析構(gòu)函數(shù)中全部進(jìn)行判斷釋放內(nèi)存。當(dāng)類中有指針成員變量,很多人喜歡在構(gòu)造函數(shù)中來動態(tài)分配初始化指針變量但常常忘了在析構(gòu)函數(shù)中來釋放內(nèi)存。當(dāng)你初來人世,父母疼愛你,當(dāng)你有了自己的小孩,你又有了生活的重心,此時父母已經(jīng)被你遺忘了,當(dāng)你的小孩又有了自己的小孩,你也就被遺忘了。我們很多程序員同志也是這樣的,使用時我要用,new得挺爽的,用完不管我事了,這明顯是違反我黨一貫有始有終的原則的。 示例代碼: class CApple { public: CApple() { m_ptrData = new char[128]; }
~CApple() { } } 上面的m_ptrData指向的內(nèi)存就這樣泄漏掉了,記得在析構(gòu)函數(shù)中加上釋放的代碼,改為如下: ~CApple() { if(NULL != m_ptrData) { delete m_ptrData; m_ptrData = NULL; } } 需要提醒的是:上面刪除m_ptrData再置NULL,是一個良好的編程習(xí)慣,可以避免產(chǎn)生野指針。(當(dāng)然這里對象都析構(gòu)了不存在這個問題,但其它很多地方將刪除的指針置NULL是非常明智的一個做法,不然鬼知道這個指針指向的內(nèi)存是否是有效的) 2. 指針容器std::vector<CType>這個錯誤也是同志們經(jīng)常犯的,其實很多時候若是簡單結(jié)構(gòu)、簡單類,你直接用std::vector<CType>就好了,能不用std::vector<CType*>就盡量不用,因為確實很容易忘了vector中原來還存放了要釋放的內(nèi)存的指針,而且在clear或是刪除一個元素時都得記起來釋放指針指向的內(nèi)容。 這個很像小時候家里收郵包,郵遞員不將郵包送到家里來,也許因為太沉了吧,只是給張包裹單要自己取領(lǐng)。今天忙,往抽屜里一扔,然后就忘了,下次又來一包裹單,又往抽屜里一扔又忘了(指針壓入vector),若你不小心將包裹單(指針)弄丟了,你自己都不知道有這么回事(忘了釋放內(nèi)存)。但現(xiàn)在就好多了,快遞公司包裹直接送到你手上。 示例代碼就不提供了,只能意會不能言傳,^_^。
3. 指針賦值若不是在定義指針代碼作用范圍內(nèi),使用其它地方定義的指針時(比如全局指針,類成員變量指針),進(jìn)行賦值操作的時候先判斷原來指針是否有值,有則先釋放原來的內(nèi)存。 因為若指針原來有值的話,你一覆蓋原來分配的內(nèi)存就再也找不到了,也就產(chǎn)生了泄漏。 代碼示例: void CMainModule::BulidList() { m_ptrList = new CList; …. } 上面的代碼,若BuildList跑到第二次時就會出問題了,此時m_ptrList本來就已經(jīng)指向一塊動態(tài)分配的內(nèi)存了,你這時不分青紅皂白再new一塊賦值過去就將前面動態(tài)分配的內(nèi)存給丟失了。 此時應(yīng)該先判斷m_ptrList是否為NULL,為NULL則new一塊內(nèi)存,否則就應(yīng)考慮重用原來的內(nèi)存或是先刪除原來再new。
4. 掃尾函數(shù)有些類型對象如CDialog,CWindow,CFile,CImage等需要在Delete前做Close、Release、Destroy等操作的,Delete時檢查是否已經(jīng)調(diào)用了相應(yīng)的掃尾函數(shù)。 這個要具體情況具體分析了,比如CDialog的子類銷毀時往往需要先調(diào)用OnDestroy或是DestroyWindow,不然就可能會存在資源泄漏的問題。
5. 公共模塊/第三方庫公共模塊一般有init()、open()和release()、terminate()、close()兩種類型的函數(shù),不要忘記掃尾類型函數(shù)的調(diào)用。 在我們這個軟件項目中就有用到一個第三方的Av.dll,主要是進(jìn)行視頻編解碼方面的庫,這個庫需要進(jìn)行初始化才能用,同時也提供了使用完關(guān)閉的方法。當(dāng)時一位同志就忘了調(diào)用掃尾函數(shù)導(dǎo)致了大量的內(nèi)存泄漏。這個就要求我們使用第三方庫時一定要看仔細(xì)使用說明,不要一味冒進(jìn)。
6. 異常分支若正常分支有內(nèi)存需要釋放,則不要忘了異常分支的內(nèi)存釋放如try語句的catch分支,函數(shù)中的多個return分支都要考慮到相應(yīng)內(nèi)存的釋放。 示例代碼: try { void *ptrData = new char[128];
/// do something … …. if(NULL != ptrData) { delete ptrData; ptrData = NULL; } } catch(CException &e) { LOG(LOG_LEVEL_ERROR, " errorcode:" << e.errorCode()); } catch(…) { LOG(LOG_LEVEL_ERROR, " errorcode:…"); }
上面的代碼就沒有考慮到兩個異常分支也應(yīng)該要判斷指針是否要進(jìn)行釋放的情況。當(dāng)跑到異常分支中去時就產(chǎn)生了內(nèi)存泄漏了,這種問題比較難查因為正常情況下程序也是正常不會有泄漏的,能編寫代碼時就注意就事半功倍了。
7. 動態(tài)分配對象數(shù)組:動態(tài)分配的對象數(shù)組,記得使用delete[]來進(jìn)行刪除。基于兩個考慮: (1)可以釋放整個數(shù)組的空間; (2)調(diào)用數(shù)組中每個對象的析構(gòu)函數(shù)。
第一個其實使用delete加上數(shù)組地址一樣是可以釋放的,因為這塊內(nèi)存是連續(xù)分配的,不論采用delete或是delete[]來釋放,操作系統(tǒng)都能將這塊連續(xù)的內(nèi)存一起釋放掉。 但第二點有什么作用呢,此時大家看看 第一章類內(nèi)成員動態(tài)分配 中的示例就知道了,很多釋放內(nèi)存的代碼是放在類的析構(gòu)函數(shù)中的,只有使用delete[]才能正確調(diào)用析構(gòu)函數(shù)。使用delete是不會調(diào)用每個數(shù)組元素的析構(gòu)函數(shù)的。
8. 非常規(guī)動態(tài)內(nèi)存分配不是采用常規(guī)內(nèi)存分配(new、malloc、calloc、realloc)的內(nèi)存也要記得釋放,如strdup等。 有一些C/C++ Api返回的指針是動態(tài)分配的需要使用者來負(fù)責(zé)釋放,這個只要使用時看清楚Api的說明就不會有什么問題了。
9. 單態(tài)模式最好在程序退出時釋放內(nèi)存,雖然OS會回收,但對于我們以后內(nèi)存泄漏檢測工作能帶來極大方便。 雖然單態(tài)模式的內(nèi)存泄漏是一次性泄漏,不會導(dǎo)致內(nèi)存的不斷增加,但因為很多內(nèi)存泄漏檢查工具都是程序正常結(jié)束后開始統(tǒng)計內(nèi)存泄漏的,此時會將單態(tài)模式的內(nèi)存泄漏也統(tǒng)計進(jìn)去。這樣我們就得一個個區(qū)分那個是單態(tài)泄漏那個是非法泄漏,會帶來很大的工作量,若能在程序退出時將單態(tài)模式的內(nèi)存泄漏也釋放掉,檢測結(jié)果就會集中在有問題的內(nèi)存泄漏上了,大大減少我們的工作量。 解決方法: 為單態(tài)模式對象定義DestroyInstance()方法用來釋放單態(tài)模式的內(nèi)存,在程序退出時調(diào)用該函數(shù)。 或是采用static的 smart 指針來讓編譯器自動在程序退出時負(fù)責(zé)釋放相應(yīng)的內(nèi)存。
10. 虛析構(gòu)函數(shù)一個類的指針被向上引用,作為基類的指針來使用的時候,把析構(gòu)函數(shù)寫成虛函數(shù)。這樣做是為了當(dāng)用一個基類的指針類型來刪除一個派生類的對象時,派生類的析構(gòu)函數(shù)會被調(diào)用。(new子類的對象,刪除時卻采用delete父類類型的指針。new CConcreteClass的對象ptr,但delete CClass類型 的指針ptr,無法調(diào)用正確的析構(gòu)函數(shù)) 當(dāng)針對接口進(jìn)行編程時,涉及到動態(tài)分配的對象指針在各函數(shù)間傳遞時特別要注意將基類的析構(gòu)函數(shù)定義成虛函數(shù)。 第一章提到了,若沒有正確的調(diào)用析構(gòu)函數(shù),析構(gòu)函數(shù)中若有釋放內(nèi)存的代碼就會得不到運(yùn)行,而且本具體子類中的一些成員變量的析構(gòu)函數(shù)也得不到執(zhí)行。因為編譯器會認(rèn)為你刪除的是一個基類類型的指針,當(dāng)然就不會去調(diào)用子類的成員變量的析構(gòu)函數(shù)的了。 代碼示例: struct ST_Info { int iWeight; char strName[128] } class CFruit { };
class CApple:public CFruit { public: std::vector< ST_Info> m_vecInfo; }
CFruit * GetApple() { CApple *ptrApple = new CApple(); ST_Info st_Info = {9, “Apple1”}; ptrApple->m_vecInfo.push_back(st_Info);
return ptrApple; } void main(int argc, char**argv) { CFruit *ptrFruit = GetApple();
delete ptrFruit; ptrFruit = NULL; }
上面的代碼就會產(chǎn)生內(nèi)存泄漏了, ptrApple->m_vecInfo中存放的內(nèi)存將全部泄漏掉,一個能為delete時認(rèn)為這是一個CFruit *的指針,不會去釋放ptrApple->m_vecInfo中元素對應(yīng)的內(nèi)存。 修正方法是只要將CFruit的析構(gòu)函數(shù)定義成虛析構(gòu)函數(shù)就OK了。
11. 線程的安全退出,user-interface thread安全退出和窗口關(guān)聯(lián)的user-interface thread 必須處理WM_DESTROY消息,建議定義一個OnDestroy()函數(shù),該函數(shù)調(diào)用PostQuitMessage(0)的方法讓user-interface thread安全退出,防止線程不安全退出導(dǎo)致內(nèi)存泄漏。 線程進(jìn)行安全退出,防止非正常退出的內(nèi)存泄漏問題。 例子: LRESULT CMsgReflect::OnDestroy(HWND hWindow, UINT uiMessage, WPARAM uiParam, LPARAM ulParam) { PostQuitMessage(0); return 0; }
12. 內(nèi)存動態(tài)分配后,在各個分支路徑均要考慮是否要釋放掉這個其實和第6章是類似的,下面的代碼就沒有考慮到執(zhí)行到continue時的情況會產(chǎn)生內(nèi)存泄漏。 for (std::vector<TeamInfo>::iterator it = e.teamlist.begin(); it != e.teamlist.end(); it++) { FriendGroupData *pGroup=new FriendGroupData; if(it->unTeamID==DEFAULT_FRIEND_GROUP_ID) continue; …. delete pGroup; } 附錄:DevPartner BoundsChecker的使用1).www .3ddown.com 網(wǎng)上可以下載到8.2版本的DevPartner,進(jìn)行安裝即可。 2).License的下載和安裝,http://download.csdn.net/source/828960,運(yùn)行Distributed License Management,將該license導(dǎo)入即可。 3).將系統(tǒng)時間改成2008年才能使用該license,此時就可以進(jìn)行內(nèi)存泄漏的檢測了,記得檢測完將系統(tǒng)時間改回來就OK了。 4).調(diào)整跟蹤堆棧的深度,在Visual Studio界面中,DevPartner->Options,然后 Error Detection->Data Collection 即可調(diào)整跟蹤堆棧的深度了。
參考資料: 【1】.《內(nèi)存泄漏的檢測、定位和解決經(jīng)驗總結(jié)》,http://blog.csdn.net/wenhm/archive/2006/06/11/787876.aspx。
|
|