9、【C++】記憶體洩露
記憶體洩露
1、記憶體洩露的定義
一般我們常說的記憶體洩漏是指堆記憶體的洩漏。堆記憶體是指程式從堆中分配的,大小任意的(記憶體塊的大小可以在程式執行期決定),使用完後必須顯示釋放的記憶體。
應用程式一般使用malloc,realloc,new等函式從堆中分配到一塊記憶體,使用完後,程式必須負責相應的呼叫free或delete釋放該記憶體塊,否則,這塊記憶體就不能被再次使用,我們就說這塊記憶體洩漏了。以下這段小程式演示了堆記憶體發生洩漏的情形:
void MyFunction(int nSize)
{
char* p= new char[nSize] ;
if( !GetStringFrom( p, nSize ) ){
MessageBox(“Error”);
return;
}
…//using the string pointed by p;
delete p;
}
當函式GetStringFrom()返回零的時候,指標p指向的記憶體就不會被釋放。這是一種常見的發生記憶體洩漏的情形。程式在入口處分配記憶體,在出口處釋放記憶體,但是c函式可以在任何地方退出,所以一旦有某個出口處沒有釋放應該釋放的記憶體,就會發生記憶體洩漏。
廣義的說,記憶體洩漏不僅僅包含堆記憶體的洩漏,還包含系統資源的洩漏(resource leak),比如核心態HANDLE,GDI Object,SOCKET, Interface等,從根本上說這些由作業系統分配的物件也消耗記憶體,如果這些物件發生洩漏最終也會導致記憶體的洩漏。而且,某些物件消耗的是核心態記憶體,這些物件嚴重洩漏時會導致整個作業系統不穩定。所以相比之下,系統資源的洩漏比堆記憶體的洩漏更為嚴重。
GDI Object的洩漏是一種常見的資源洩漏:
void CMyView::OnPaint( CDC* pDC )
{
CBitmap bmp;
CBitmap* pOldBmp;
bmp.LoadBitmap(IDB_MYBMP);
pOldBmp = pDC->SelectObject( &bmp );
if( Something() ){
return;
}
pDC->SelectObject( pOldBmp );
return;
}
當函式Something()返回非零的時候,程式在退出前沒有把pOldBmp選回pDC中,這會導致pOldBmp指向的HBITMAP物件發生洩漏。這個程式如果長時間的執行,可能會導致整個系統花屏。這種問題在Win9x下比較容易暴露出來,因為Win9x的GDI堆比Win2k或NT的要小很多。
2、記憶體洩漏後果
只發生一次的小的記憶體洩漏可能不會被注意,但洩漏大量記憶體的程式或洩漏日益增多的程式可能會表現出各種徵兆:從效能不良(並且逐漸降低)到記憶體完全用盡。
更糟的是,洩漏的程式可能會用掉太多記憶體,以致另一個程式失敗,而使使用者無從查詢問題的真正根源。 此外,即使無害的記憶體洩漏也可能是其他問題的徵兆。記憶體洩漏會因為減少可用記憶體的數量從而降低計算機的效能。
記憶體洩漏也會導致較嚴重的後果:
- 程式執行後置之不理,並且隨著時間的流失消耗越來越多的記憶體(比如伺服器上的後臺任務,尤其是嵌入式系統中的後臺任務,這些任務可能被執行後很多年內都置之不理);
- 新的記憶體被頻繁地分配,比如當顯示電腦遊戲或動畫視訊畫面時;
- 程式能夠請求未被釋放的記憶體(比如共享記憶體),甚至是在程式終止的時候;
- 洩漏在作業系統內部發生;
- 洩漏在系統關鍵驅動中發生;
- 記憶體非常有限,比如在嵌入式系統或便攜裝置中;
- 當運行於一個終止時記憶體並不自動釋放的作業系統(比如AmigaOS)之上,而且一旦丟失只能通過重啟來恢復。
3、記憶體洩漏的幾種情況
(1)在類的建構函式和解構函式中沒有匹配的呼叫new和delete函式
兩種情況下會出現這種記憶體洩露:
一是在堆裡建立了物件佔用了記憶體,但是沒有顯示地釋放物件佔用的記憶體;
二是在類的建構函式中動態的分配了記憶體,但是在解構函式中沒有釋放記憶體或者沒有正確的釋放記憶體;
(2)沒有正確的清除巢狀物件的指標
(3)在釋放物件陣列時在delete中沒有使用方括號
方括號是告訴編譯器這個指標指向的是一個物件陣列,同時也告訴編譯器正確的物件地址值病呼叫物件的解構函式,如果沒有方括號,那麼這個指標就被預設為只指向一個物件,物件陣列中的其他物件的解構函式就不會被呼叫,結果造成了記憶體洩露。
如果在方括號中間放了一個比物件陣列大小還大的數字,那麼編譯器就會呼叫無效物件(記憶體溢位)的解構函式,會造成堆的奔潰。如果方括號中間的數字值比物件陣列的大小小的話,編譯器就不能呼叫足夠多個解構函式,結果會造成記憶體洩露。
釋放單個物件、單個基本資料型別的變數或者是基本資料型別的陣列不需要大小引數,釋放定義了解構函式的物件陣列才需要大小引數。
(4)指向物件的指標陣列不等同於物件陣列
物件陣列是指:陣列中存放的是物件,只需要delete []p,即可呼叫物件陣列中的每個物件的解構函式釋放空間;
指向物件的指標陣列是指:陣列中存放的是指向物件的指標,不僅要釋放每個物件的空間,還要釋放每個指標的空間,delete []p只是釋放了每個指標,但是並沒有釋放物件的空間,正確的做法,是通過一個迴圈,將每個物件釋放了,然後再把指標釋放了;
(5) 缺少拷貝建構函式
兩次釋放相同的記憶體是一種錯誤的做法,同時可能會造成堆的崩潰。按值傳遞會呼叫(拷貝)建構函式,引用傳遞不會呼叫。
在C++中,如果沒有定義拷貝建構函式,那麼編譯器就會呼叫預設的拷貝建構函式,會逐個成員拷貝的方式來複制資料成員,如果是以逐個成員拷貝的方式來複制指標被定義為將一個變數的地址賦給另一個變數。
這種隱式的指標複製結果就是兩個物件擁有指向同一個動態分配的記憶體空間的指標:
當釋放第一個物件的時候,它的解構函式就會釋放與該物件有關的動態分配的記憶體空間。而釋放第二個物件的時候,它的解構函式會釋放相同的記憶體,這樣是錯誤的。所以,如果一個類裡面有指標成員變數,要麼必須顯示的寫拷貝建構函式和過載賦值運算子,要麼禁用拷貝建構函式和過載賦值運算子
(6) 缺少過載賦值運算子
這種問題跟上述問題類似,也是逐個成員拷貝的方式複製物件,如果這個類的大小是可變的,那麼結果就是造成記憶體洩露;
(7) 關於nonmodifying運算子過載的常見迷思
a. 返回棧上物件的引用或者指標(也即返回區域性物件的引用或者指標)。導致最後返回的是一個空引用或者空指標,因此變成野指標;
b. 返回內部靜態物件的引用;
c. 返回一個洩露記憶體的動態分配的物件。導致記憶體洩露,並且無法回收;
解決這一類問題的辦法是過載運算子函式的返回值不是型別的引用,二應該是型別的返回值,即不是 int&而是int;
(8)沒有將基類的解構函式定義為虛擬函式
當基類指標指向子類物件時,如果基類的解構函式不是virtual,那麼子類的解構函式將不會被呼叫,子類的資源沒有正確是釋放,因此造成記憶體洩露;
4、野指標
野指標:指向被釋放的或者訪問受限記憶體的指標。
造成野指標的原因:
1. 指標變數沒有被初始化(如果值不定,可以初始化為NULL);
2. 指標被free或者delete後,沒有置為NULL, free和delete只是把指標所指向的記憶體給釋放掉,並沒有把指標本身幹掉,此時指標指向的是“垃圾”記憶體。釋放後的指標應該被置為NULL;
3. 指標操作超越了變數的作用範圍,比如返回指向棧記憶體的指標就是野指標;
記憶體洩露總結
其實記憶體洩漏的原因可以概括為:呼叫了malloc/new等記憶體申請的操作,但缺少了對應的free/delete釋放操作,總之就是,malloc/new比free/delete的數量多。記憶體用完,不再使用要及時釋放。