一、內存錯誤的分類
a.內存訪問錯誤
對內存進行讀或寫時發生的錯誤,可能是讀未被初始化的內存單元,也可能是讀寫錯誤的內存單元。
b.內存使用錯誤
主要是在動態請求內存之後沒有正確釋放產生的錯誤。
二、內存剖析(典型的c++內存模型)
BSS段:BSS段(bss segment)通常是指用來存放程序中未初始化的全局變量的一塊內存區域。BSS是英文Block Started by Symbol的簡稱。BSS段屬於靜態內存分配。
數據段:數據段(data segment)通常是指用來存放程序中已初始化的全局變量的一塊內存區域。數據段屬於靜態內存分配。(其實我不太明白既然都是存全局變量的,那爲什麼要把已初始化的和未初始化的分開在兩個段中進行管理)
代碼段:代碼段(code segment/text segment)通常是指用來存放程序執行代碼的一塊內存區域。這部分區域的大小在程序運行前就已經確定,並且內存區域通常屬於只讀, 某些架構也允許代碼段爲可寫,即允許修改程序。在代碼段中,也有可能包含一些只讀的常數變量,例如字符串常量等。
堆(heap):堆是用於存放進程運行中被動態分配的內存段,它的大小並不固定,可動態擴張或縮減。當進程調用malloc等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)
棧(stack):棧又稱堆棧, 是用戶存放程序臨時創建的局部變量,也就是說我們函數括弧“{}”中定義的變量(但不包括static聲明的變量,static意味着在數據段中存放變量)。除此以外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,並且待到調用結束後,函數的返回值也會被存放回棧中。由於棧的先進先出特點,所以棧特別方便用來保存/恢復調用現場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時數據的內存區。
c++不同於C#、Java的一個地方是它可以動態管理內存,但魚與熊掌兩者不可兼得,靈活性的代價是程序員需要花費更多的精力保證代碼不發生內存錯誤。
三、常見的內存訪問錯誤和內存使用錯誤
具體來說,內存訪問錯誤有下面這幾種:訪問未被初始化的內存單元、數組訪問錯誤、訪問無效的內存單元(0x000000,0x000005等)、寫無效內存。
而內存使用錯誤有:1、請求內存之後沒有將它釋放,使new和delete成對出現可以避免這樣的問題。2、釋放一塊內存後又再釋放一次。
四、例子
- #include <iostream>
- using namespace std;
- int main()
- {
- char* str1="four";
- char* str2=new char[4]; //not enough space
- char* str3=str2;
- cout<<str2<<endl; //UMR
- strcpy(str2,str1); //ABW
- cout<<str2<<endl; //ABR
- delete str2;
- str2[0]+=2; //FMR and FMW
- delete str3; //FFM
- }
ABW:Array Bound Write.數組越界寫
FMR/W:Freed Memery Read/Write.讀/寫已被釋放的內存
FFM:Free Freed Memery.釋放已被釋放的內存
由以上的程序,我們可以看到:在第5行分配內存時,忽略了字符串終止符"/0"所佔空間導致了第8行的數組越界寫(Array Bounds Write)和第9行的數組越界讀(Array Bounds Read); 在第7行,打印尚未賦值的str2將產生訪問未初始化內存錯誤(Uninitialized Memory Read);在第11行使用已經釋放的變量將導致釋放內存讀和寫錯誤(Freed Memory Read and Freed Memory Write);最後由於str3和str2所指的是同一片內存,第12行又一次釋放了已經被釋放的空間 (Free Freed Memory)。
這個包含許多錯誤的程序可以編譯連接,而且可以在很多平臺上運行。但是這些錯誤就像定時炸彈,會在特殊配置下觸發,造成不可預見的錯誤。這就是內存錯誤難以發現的一個主要原因。
內存泄漏的定義
一般我們常說的內存泄漏是指堆內存的泄漏。堆內存是指程序從堆中分配的,大小任意的(內存塊的大小可以在程序運行期決定),使用完後必須顯示釋放的內存。應用程序一般使用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的要小很多。
內存泄漏的發生方式:
以發生的方式來分類,內存泄漏可以分爲4類:
1. 常發性內存泄漏。發生內存泄漏的代碼會被多次執行到,每次被執行的時候都會導致一塊內存泄漏。比如例二,如果Something()函數一直返回True,那麼pOldBmp指向的HBITMAP對象總是發生泄漏。
2. 偶發性內存泄漏。發生內存泄漏的代碼只有在某些特定環境或操作過程下才會發生。比如例二,如果Something()函數只有在特定環境下才返回True,那麼pOldBmp指向的HBITMAP對象並不總是發生泄漏。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測內存泄漏至關重要。
3. 一次性內存泄漏。發生內存泄漏的代碼只會被執行一次,或者由於算法上的缺陷,導致總會有一塊僅且一塊內存發生泄漏。比如,在類的構造函數中分配內存,在析構函數中卻沒有釋放該內存,但是因爲這個類是一個Singleton,所以內存泄漏只會發生一次。另一個例子:
- char* g_lpszFileName = NULL;
- void SetFileName( const char* lpcszFileName )
- {
- if( g_lpszFileName ){
- free( g_lpszFileName );
- }
- g_lpszFileName = strdup( lpcszFileName );
- }
例三
如果程序在結束的時候沒有釋放g_lpszFileName指向的字符串,那麼,即使多次調用SetFileName(),總會有一塊內存,而且僅有一塊內存發生泄漏。
4. 隱式內存泄漏。程序在運行過程中不停的分配內存,但是直到結束的時候才釋放內存。嚴格的說這裏並沒有發生內存泄漏,因爲最終程序釋放了所有申請的內存。但是對於一個服務器程序,需要運行幾天,幾周甚至幾個月,不及時釋放內存也可能導致最終耗盡系統的所有內存。所以,我們稱這類內存泄漏爲隱式內存泄漏。舉一個例子:
- class Connection
- {
- public:
- Connection( SOCKET s);
- ~Connection();
- …
- private:
- SOCKET _socket;
- …
- };
- class ConnectionManager
- {
- public:
- ConnectionManager(){}
- ~ConnectionManager(){
- list::iterator it;
- for( it = _connlist.begin(); it != _connlist.end(); ++it ){
- delete (*it);
- }
- _connlist.clear();
- }
- void OnClientConnected( SOCKET s ){
- Connection* p = new Connection(s);
- _connlist.push_back(p);
- }
- void OnClientDisconnected( Connection* pconn ){
- _connlist.remove( pconn );
- delete pconn;
- }
- private:
- list _connlist;
- };
例四
假設在Client從Server端斷開後,Server並沒有呼叫OnClientDisconnected()函數,那麼代表那次連接的Connection對象就不會被及時的刪除(在Server程序退出的時候,所有Connection對象會在ConnectionManager的析構函數裏被刪除)。當不斷的有連接建立、斷開時隱式內存泄漏就發生了。