C++内存管理与内存泄漏及其检测
一、內(nèi)存錯(cuò)誤的分類(lèi)
a.內(nèi)存訪問(wèn)錯(cuò)誤
對(duì)內(nèi)存進(jìn)行讀或?qū)憰r(shí)發(fā)生的錯(cuò)誤,可能是讀未被初始化的內(nèi)存單元,也可能是讀寫(xiě)錯(cuò)誤的內(nèi)存單元。??
b.內(nèi)存使用錯(cuò)誤
主要是在動(dòng)態(tài)請(qǐng)求內(nèi)存之后沒(méi)有正確釋放產(chǎn)生的錯(cuò)誤。
二、內(nèi)存剖析(典型的c++內(nèi)存模型)
?
BSS段:BSS段(bss segment)通常是指用來(lái)存放程序中未初始化的全局變量的一塊內(nèi)存區(qū)域。BSS是英文Block Started by Symbol的簡(jiǎn)稱(chēng)。BSS段屬于靜態(tài)內(nèi)存分配。
數(shù)據(jù)段:數(shù)據(jù)段(data segment)通常是指用來(lái)存放程序中已初始化的全局變量的一塊內(nèi)存區(qū)域。數(shù)據(jù)段屬于靜態(tài)內(nèi)存分配。(其實(shí)我不太明白既然都是存全局變量的,那為什么要把已初始化的和未初始化的分開(kāi)在兩個(gè)段中進(jìn)行管理)
代碼段:代碼段(code segment/text segment)通常是指用來(lái)存放程序執(zhí)行代碼的一塊內(nèi)存區(qū)域。這部分區(qū)域的大小在程序運(yùn)行前就已經(jīng)確定,并且內(nèi)存區(qū)域通常屬于只讀, 某些架構(gòu)也允許代碼段為可寫(xiě),即允許修改程序。在代碼段中,也有可能包含一些只讀的常數(shù)變量,例如字符串常量等。
堆(heap):堆是用于存放進(jìn)程運(yùn)行中被動(dòng)態(tài)分配的內(nèi)存段,它的大小并不固定,可動(dòng)態(tài)擴(kuò)張或縮減。當(dāng)進(jìn)程調(diào)用malloc等函數(shù)分配內(nèi)存時(shí),新分配的內(nèi)存就被動(dòng)態(tài)添加到堆上(堆被擴(kuò)張);當(dāng)利用free等函數(shù)釋放內(nèi)存時(shí),被釋放的內(nèi)存從堆中被剔除(堆被縮減)
棧(stack):棧又稱(chēng)堆棧, 是用戶(hù)存放程序臨時(shí)創(chuàng)建的局部變量,也就是說(shuō)我們函數(shù)括弧“{}”中定義的變量(但不包括static聲明的變量,static意味著在數(shù)據(jù)段中存放變量)。除此以外,在函數(shù)被調(diào)用時(shí),其參數(shù)也會(huì)被壓入發(fā)起調(diào)用的進(jìn)程棧中,并且待到調(diào)用結(jié)束后,函數(shù)的返回值也會(huì)被存放回棧中。由于棧的先進(jìn)先出特點(diǎn),所以棧特別方便用來(lái)保存/恢復(fù)調(diào)用現(xiàn)場(chǎng)。從這個(gè)意義上講,我們可以把堆棧看成一個(gè)寄存、交換臨時(shí)數(shù)據(jù)的內(nèi)存區(qū)。
c++不同于C#、Java的一個(gè)地方是它可以動(dòng)態(tài)管理內(nèi)存,但魚(yú)與熊掌兩者不可兼得,靈活性的代價(jià)是程序員需要花費(fèi)更多的精力保證代碼不發(fā)生內(nèi)存錯(cuò)誤。
三、常見(jiàn)的內(nèi)存訪問(wèn)錯(cuò)誤和內(nèi)存使用錯(cuò)誤
具體來(lái)說(shuō),內(nèi)存訪問(wèn)錯(cuò)誤有下面這幾種:訪問(wèn)未被初始化的內(nèi)存單元、數(shù)組訪問(wèn)錯(cuò)誤、訪問(wèn)無(wú)效的內(nèi)存單元(0x000000,0x000005等)、寫(xiě)無(wú)效內(nèi)存。
而內(nèi)存使用錯(cuò)誤有:1、請(qǐng)求內(nèi)存之后沒(méi)有將它釋放,使new和delete成對(duì)出現(xiàn)可以避免這樣的問(wèn)題。2、釋放一塊內(nèi)存后又再釋放一次。
四、例子
?#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???
}??
UMR:Uninitialized Memery Read.讀未初始化內(nèi)存???
ABW:Array Bound Write.數(shù)組越界寫(xiě)
FMR/W:Freed Memery Read/Write.讀/寫(xiě)已被釋放的內(nèi)存
FFM:Free Freed Memery.釋放已被釋放的內(nèi)存????????
??? 由以上的程序,我們可以看到:在第5行分配內(nèi)存時(shí),忽略了字符串終止符"\0"所占空間導(dǎo)致了第8行的數(shù)組越界寫(xiě)(Array Bounds Write)和第9行的數(shù)組越界讀(Array Bounds Read); 在第7行,打印尚未賦值的str2將產(chǎn)生訪問(wèn)未初始化內(nèi)存錯(cuò)誤(Uninitialized Memory Read);在第11行使用已經(jīng)釋放的變量將導(dǎo)致釋放內(nèi)存讀和寫(xiě)錯(cuò)誤(Freed Memory Read and Freed Memory Write);最后由于str3和str2所指的是同一片內(nèi)存,第12行又一次釋放了已經(jīng)被釋放的空間 (Free Freed Memory)。
這個(gè)包含許多錯(cuò)誤的程序可以編譯連接,而且可以在很多平臺(tái)上運(yùn)行。但是這些錯(cuò)誤就像定時(shí)炸彈,會(huì)在特殊配置下觸發(fā),造成不可預(yù)見(jiàn)的錯(cuò)誤。這就是內(nèi)存錯(cuò)誤難以發(fā)現(xiàn)的一個(gè)主要原因。
?
內(nèi)存泄漏的定義
一般我們常說(shuō)的內(nèi)存泄漏是指堆內(nèi)存的泄漏。堆內(nèi)存是指程序從堆中分配的,大小任意的(內(nèi)存塊的大小可以在程序運(yùn)行期決定),使用完后必須顯示釋放的內(nèi)存。應(yīng)用程序一般使用malloc,realloc,new等函數(shù)從堆中分配到一塊內(nèi)存,使用完后,程序必須負(fù)責(zé)相應(yīng)的調(diào)用free或delete釋放該內(nèi)存塊,否則,這塊內(nèi)存就不能被再次使用,我們就說(shuō)這塊內(nèi)存泄漏了。以下這段小程序演示了堆內(nèi)存發(fā)生泄漏的情形:
?void MyFunction(int nSize)
{
char* p= new char[nSize];
if( !GetStringFrom( p, nSize ) ){
MessageBox(“Error”);
return;
}
…//using the string pointed by p;
delete p;
}
例一
當(dāng)函數(shù)GetStringFrom()返回零的時(shí)候,指針p指向的內(nèi)存就不會(huì)被釋放。這是一種常見(jiàn)的發(fā)生內(nèi)存泄漏的情形。程序在入口處分配內(nèi)存,在出口處釋放內(nèi)存,但是c函數(shù)可以在任何地方退出,所以一旦有某個(gè)出口處沒(méi)有釋放應(yīng)該釋放的內(nèi)存,就會(huì)發(fā)生內(nèi)存泄漏。
廣義的說(shuō),內(nèi)存泄漏不僅僅包含堆內(nèi)存的泄漏,還包含系統(tǒng)資源的泄漏(resource leak),比如核心態(tài)HANDLE,GDI Object,SOCKET, Interface等,從根本上說(shuō)這些由操作系統(tǒng)分配的對(duì)象也消耗內(nèi)存,如果這些對(duì)象發(fā)生泄漏最終也會(huì)導(dǎo)致內(nèi)存的泄漏。而且,某些對(duì)象消耗的是核心態(tài)內(nèi)存,這些對(duì)象嚴(yán)重泄漏時(shí)會(huì)導(dǎo)致整個(gè)操作系統(tǒng)不穩(wěn)定。所以相比之下,系統(tǒng)資源的泄漏比堆內(nèi)存的泄漏更為嚴(yán)重。
GDI Object的泄漏是一種常見(jiàn)的資源泄漏:
?void CMyView::OnPaint( CDC* pDC )
{
CBitmap bmp;
CBitmap* pOldBmp;
bmp.LoadBitmap(IDB_MYBMP);
pOldBmp = pDC->SelectObject( &bmp );
…
if( Something() ){
return;
}
pDC->SelectObject( pOldBmp );
return;
}
例二
當(dāng)函數(shù)Something()返回非零的時(shí)候,程序在退出前沒(méi)有把pOldBmp選回pDC中,這會(huì)導(dǎo)致pOldBmp指向的HBITMAP對(duì)象發(fā)生泄漏。這個(gè)程序如果長(zhǎng)時(shí)間的運(yùn)行,可能會(huì)導(dǎo)致整個(gè)系統(tǒng)花屏。這種問(wèn)題在Win9x下比較容易暴露出來(lái),因?yàn)閃in9x的GDI堆比Win2k或NT的要小很多。
內(nèi)存泄漏的發(fā)生方式:
以發(fā)生的方式來(lái)分類(lèi),內(nèi)存泄漏可以分為4類(lèi):
1. 常發(fā)性?xún)?nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼會(huì)被多次執(zhí)行到,每次被執(zhí)行的時(shí)候都會(huì)導(dǎo)致一塊內(nèi)存泄漏。比如例二,如果Something()函數(shù)一直返回True,那么pOldBmp指向的HBITMAP對(duì)象總是發(fā)生泄漏。
2. 偶發(fā)性?xún)?nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只有在某些特定環(huán)境或操作過(guò)程下才會(huì)發(fā)生。比如例二,如果Something()函數(shù)只有在特定環(huán)境下才返回True,那么pOldBmp指向的HBITMAP對(duì)象并不總是發(fā)生泄漏。常發(fā)性和偶發(fā)性是相對(duì)的。對(duì)于特定的環(huán)境,偶發(fā)性的也許就變成了常發(fā)性的。所以測(cè)試環(huán)境和測(cè)試方法對(duì)檢測(cè)內(nèi)存泄漏至關(guān)重要。
3. 一次性?xún)?nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只會(huì)被執(zhí)行一次,或者由于算法上的缺陷,導(dǎo)致總會(huì)有一塊僅且一塊內(nèi)存發(fā)生泄漏。比如,在類(lèi)的構(gòu)造函數(shù)中分配內(nèi)存,在析構(gòu)函數(shù)中卻沒(méi)有釋放該內(nèi)存,但是因?yàn)檫@個(gè)類(lèi)是一個(gè)Singleton,所以?xún)?nèi)存泄漏只會(huì)發(fā)生一次。另一個(gè)例子:
?
?char* g_lpszFileName = NULL;
void SetFileName( const char* lpcszFileName )
{
if( g_lpszFileName ){
free( g_lpszFileName );
}
g_lpszFileName = strdup( lpcszFileName );
}
例三
如果程序在結(jié)束的時(shí)候沒(méi)有釋放g_lpszFileName指向的字符串,那么,即使多次調(diào)用SetFileName(),總會(huì)有一塊內(nèi)存,而且僅有一塊內(nèi)存發(fā)生泄漏。
4. 隱式內(nèi)存泄漏。程序在運(yùn)行過(guò)程中不停的分配內(nèi)存,但是直到結(jié)束的時(shí)候才釋放內(nèi)存。嚴(yán)格的說(shuō)這里并沒(méi)有發(fā)生內(nèi)存泄漏,因?yàn)樽罱K程序釋放了所有申請(qǐng)的內(nèi)存。但是對(duì)于一個(gè)服務(wù)器程序,需要運(yùn)行幾天,幾周甚至幾個(gè)月,不及時(shí)釋放內(nèi)存也可能導(dǎo)致最終耗盡系統(tǒng)的所有內(nèi)存。所以,我們稱(chēng)這類(lèi)內(nèi)存泄漏為隱式內(nèi)存泄漏。舉一個(gè)例子:
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;
};
例四
假設(shè)在Client從Server端斷開(kāi)后,Server并沒(méi)有呼叫OnClientDisconnected()函數(shù),那么代表那次連接的Connection對(duì)象就不會(huì)被及時(shí)的刪除(在Server程序退出的時(shí)候,所有Connection對(duì)象會(huì)在ConnectionManager的析構(gòu)函數(shù)里被刪除)。當(dāng)不斷的有連接建立、斷開(kāi)時(shí)隱式內(nèi)存泄漏就發(fā)生了。
文章出處:http://www.diybl.com/course/3_program/c++/cppjs/20081124/152540_2.html
?
文章出處:http://www.diybl.com/course/3_program/c++/cppjs/20081124/152540.html
轉(zhuǎn)載于:https://www.cnblogs.com/traveller/archive/2009/04/15/1436506.html
總結(jié)
以上是生活随笔為你收集整理的C++内存管理与内存泄漏及其检测的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: C盘下什么文件能删除?
- 下一篇: 【推荐】《精通.NET互操作:P/Inv