引言
C++的高抽象層次,又兼具高性能,是其他語言所無法替代的,C++標(biāo)準(zhǔn)保持穩(wěn)定發(fā)展,更加現(xiàn)代化,更加強(qiáng)大。但在各種活躍編程語言中,C++門檻依然很高,尤其C++的內(nèi)存問題(內(nèi)存泄露,內(nèi)存溢出,內(nèi)存宕機(jī),堆棧破壞等問題),需要理解C++標(biāo)準(zhǔn)對象模型,C++標(biāo)準(zhǔn)庫,標(biāo)準(zhǔn)C庫,操作系統(tǒng)等內(nèi)存設(shè)計(jì),才能更加深入理解C++內(nèi)存管理。
一、C++的內(nèi)存布局
如圖,描述了C++程序的內(nèi)存分布。
Code Segment(代碼區(qū)):也稱Text Segment,存放可執(zhí)行程序的機(jī)器碼。
Data Segment (數(shù)據(jù)區(qū)):存放已初始化的全局和靜態(tài)變量, 常量數(shù)據(jù)(如字符串常量)。
BSS(Block started by symbol):存放未初始化的全局和靜態(tài)變量。(默認(rèn)設(shè)為0)
Heap(堆):從低地址向高地址增長。容量大于棧,程序中動態(tài)分配的內(nèi)存在此區(qū)域。
Stack(棧):從高地址向低地址增長。由編譯器自動管理分配。程序中的局部變量、函數(shù)參數(shù)值、返回變量等存在此區(qū)域。
?1.1 函數(shù)棧
如上圖所示,可執(zhí)行程序的文件包含BSS,Data Segment和Code Segment,當(dāng)可執(zhí)行程序載入內(nèi)存后,系統(tǒng)會保留一些空間,即堆區(qū)和棧區(qū)。堆區(qū)主要是動態(tài)分配的內(nèi)存(默認(rèn)情況下),而棧區(qū)主要是函數(shù)以及局部變量等(包括main函數(shù))。一般而言,棧的空間遠(yuǎn)遠(yuǎn)堆的空間。
當(dāng)調(diào)用函數(shù)時,一塊連續(xù)內(nèi)存(堆棧幀)壓入棧;函數(shù)返回時,堆棧幀彈出。
堆棧幀包含如下數(shù)據(jù):
① 函數(shù)返回地址
② 局部變量/CPU寄存器數(shù)據(jù)備份
1.2 全局變量
當(dāng)全局/靜態(tài)變量(如下代碼中的x和y變量)未初始化的時候,它們記錄在BSS段。
int x;
int z = 5;
void func()
{
static int y;
}
int main()
{
return 0;
}
處于BSS段的變量的值默認(rèn)為0,考慮到這一點(diǎn),BSS段內(nèi)部無需存儲大量的零值,而只需記錄字節(jié)個數(shù)即可。
系統(tǒng)載入可執(zhí)行程序后,將BSS段的數(shù)據(jù)載入數(shù)據(jù)段(Data Segment) ,并將內(nèi)存初始化為0,再調(diào)用程序入口(main函數(shù))。而對于已經(jīng)初始化了的全局/靜態(tài)變量而言,如以上代碼中的z變量,則一直存儲于數(shù)據(jù)段(Data Segment)。
1.2 內(nèi)存對齊
對于基礎(chǔ)類型,如float, double, int, char等,它們的大小和內(nèi)存占用是一致的。而對于結(jié)構(gòu)體而言,如果我們?nèi)〉闷鋝izeof的結(jié)果,會發(fā)現(xiàn)這個值有可能會大于結(jié)構(gòu)體內(nèi)所有成員大小的總和,這是由于結(jié)構(gòu)體內(nèi)部成員進(jìn)行了內(nèi)存對齊
1.2.1 為什么要內(nèi)存對齊
① 內(nèi)存對齊使數(shù)據(jù)讀取更高效
在硬件設(shè)計(jì)上,數(shù)據(jù)讀取的處理器只能從地址為k的倍數(shù)的內(nèi)存處開始讀取數(shù)據(jù)。這種讀取方式相當(dāng)于將內(nèi)存分為了多個"塊“,假設(shè)內(nèi)存可以從任意位置開始存放的話,數(shù)據(jù)很可能會被分散到多個“塊”中,處理分散在多個塊中的數(shù)據(jù)需要移除首尾不需要的字節(jié),再進(jìn)行合并,非常耗時。
為了提高數(shù)據(jù)讀取的效率,程序分配的內(nèi)存并不是連續(xù)存儲的,而是按首地址為k的倍數(shù)的方式存儲;這樣就可以一次性讀取數(shù)據(jù),而不需要額外的操作。
② 在某些平臺下,不進(jìn)行內(nèi)存對齊會崩潰?
1.2.2如何不使用內(nèi)存對齊
當(dāng)然有一些地方為了減少內(nèi)存的開銷就會可以避免內(nèi)存對齊,比如SDS使用專門的編譯優(yōu)化來節(jié)省內(nèi)存空間?:
struct __attribute__ ((__packed__)) sdshdr8
attribute ((packed))的作用就是告訴編譯器,在編譯 sdshdr8 結(jié)構(gòu)時,不要使用字節(jié)對齊的方式,而是采用緊湊的方式分配內(nèi)存。這是因?yàn)樵谀J(rèn)情況下,編譯器會按照 8 字節(jié)對齊的方式,給變量分配內(nèi)存。也就是說,即使一個變量的大小不到 8 個字節(jié),編譯器也會給它分配 8 個字節(jié)。
為了節(jié)省內(nèi)存,Redis 在這方面的設(shè)計(jì)上可以說是精打細(xì)算的。所以,Redis 采用了attribute ((packed))屬性定義結(jié)構(gòu)體,這樣一來,結(jié)構(gòu)體實(shí)際占用多少內(nèi)存空間,編譯器就分配多少空間。
比如,我用attribute ((packed))屬性定義結(jié)構(gòu)體 s2,同樣包含 char 和 int 兩個類型的成員變量,代碼如下所示:
#include <stdio.h>
int main() {
struct __attribute__((packed)) s2{
char a;
int b;
} ts2;
printf("%lu\n", sizeof(ts2));
return 0;
}
當(dāng)你運(yùn)行這段代碼時,你可以看到,打印的結(jié)果是 5,表示編譯器用了緊湊型內(nèi)存分配,s2 結(jié)構(gòu)體只占用 5 個字節(jié)的空間。
總而言之,如果你在開發(fā)程序時,希望能節(jié)省數(shù)據(jù)結(jié)構(gòu)的內(nèi)存開銷,就可以把a(bǔ)ttribute ((packed))這個編程方法用起來。
1.2.3?內(nèi)存對齊的規(guī)則
定義有效對齊值(alignment)為結(jié)構(gòu)體中 最寬成員 和 編譯器/用戶指定對齊值 中較小的那個。
(1) 結(jié)構(gòu)體起始地址為有效對齊值的整數(shù)倍
(2) 結(jié)構(gòu)體總大小為有效對齊值的整數(shù)倍
(3) 結(jié)構(gòu)體第一個成員偏移值為0,之后成員的偏移值為 min(有效對齊值, 自身大小) 的整數(shù)倍
相當(dāng)于每個成員要進(jìn)行對齊,并且整個結(jié)構(gòu)體也需要進(jìn)行對齊。
struct A
{
int i;
char c1;
char c2;
};
int main()
{
cout << sizeof(A) << endl; // 有效對齊值為4, output : 8
return 0;
}
1.3 內(nèi)存碎片
程序的內(nèi)存往往不是緊湊連續(xù)排布的,而是存在著許多碎片。我們根據(jù)碎片產(chǎn)生的原因把碎片分為內(nèi)部碎片和外部碎片兩種類型:
(1) 內(nèi)部碎片:系統(tǒng)分配的內(nèi)存大于實(shí)際所需的內(nèi)存(由于對齊機(jī)制);
(2) 外部碎片:不斷分配回收不同大小的內(nèi)存,由于內(nèi)存分布散亂,較大內(nèi)存無法分配;
1.4 繼承類布局?
繼承
如果一個類繼承自另一個類,那么它自身的數(shù)據(jù)位于父類之后。
含虛函數(shù)的類
如果當(dāng)前類包含虛函數(shù),則會在類的最前端占用4個字節(jié),用于存儲虛表指針(vpointer),它指向一個虛函數(shù)表(vtable)。vtable中包含當(dāng)前類的所有虛函數(shù)指針。
二、C++內(nèi)存泄漏
2.1 什么是內(nèi)存
一般我們常說的內(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)存就不能被再次使用,我們就說這塊內(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()返回零的時候,指針p指向的內(nèi)存就不會被釋放。這是一種常見的發(fā)生內(nèi)存泄漏的情形。程序在入口處分配內(nèi)存,在出口處釋放內(nèi)存,但是c函數(shù)可以在任何地方退出,所以一旦有某個出口處沒有釋放應(yīng)該釋放的內(nèi)存,就會發(fā)生內(nèi)存泄漏。
廣義的說,內(nèi)存泄漏不僅僅包含堆內(nèi)存的泄漏,還包含系統(tǒng)資源的泄漏(resource leak),比如核心態(tài)HANDLE,GDI Object,SOCKET,?Interface等,從根本上說這些由操作系統(tǒng)分配的對象也消耗內(nèi)存,如果這些對象發(fā)生泄漏最終也會導(dǎo)致內(nèi)存的泄漏。而且,某些對象消耗的是核心態(tài)內(nèi)存,這些對象嚴(yán)重泄漏時會導(dǎo)致整個操作系統(tǒng)不穩(wěn)定。
2.2 內(nèi)存泄漏的發(fā)生方式
以發(fā)生的方式來分類,內(nèi)存泄漏可以分為4類:
1.?常發(fā)性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼會被多次執(zhí)行到,每次被執(zhí)行的時候都會導(dǎo)致一塊內(nèi)存泄漏。比如例二,如果Something()函數(shù)一直返回True,那么pOldBmp指向的HBITMAP對象總是發(fā)生泄漏。
2.?偶發(fā)性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只有在某些特定環(huán)境或操作過程下才會發(fā)生。比如例二,如果Something()函數(shù)只有在特定環(huán)境下才返回True,那么pOldBmp指向的HBITMAP對象并不總是發(fā)生泄漏。常發(fā)性和偶發(fā)性是相對的。對于特定的環(huán)境,偶發(fā)性的也許就變成了常發(fā)性的。所以測試環(huán)境和測試方法對檢測內(nèi)存泄漏至關(guān)重要。
? ? ? ?3.?一次性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只會被執(zhí)行一次,或者由于算法上的缺陷,導(dǎo)致總會有一塊僅且一塊內(nèi)存發(fā)生泄漏。比如,在類的構(gòu)造函數(shù)中分配內(nèi)存,在析構(gòu)函數(shù)中卻沒有釋放該內(nèi)存,但是因?yàn)檫@個類是一個Singleton,所以內(nèi)存泄漏只會發(fā)生一次。另一個例子:
char* g_lpszFileName = NULL;
void SetFileName( const char* lpcszFileName )
{
if( g_lpszFileName ){
free( g_lpszFileName );
}
g_lpszFileName = strdup( lpcszFileName );
}
如果程序在結(jié)束的時候沒有釋放g_lpszFileName指向的字符串,那么,即使多次調(diào)用SetFileName(),總會有一塊內(nèi)存,而且僅有一塊內(nèi)存發(fā)生泄漏。
4.?隱式內(nèi)存泄漏。程序在運(yùn)行過程中不停的分配內(nèi)存,但是直到結(jié)束的時候才釋放內(nèi)存。嚴(yán)格的說這里并沒有發(fā)生內(nèi)存泄漏,因?yàn)樽罱K程序釋放了所有申請的內(nèi)存。但是對于一個服務(wù)器程序,需要運(yùn)行幾天,幾周甚至幾個月,不及時釋放內(nèi)存也可能導(dǎo)致最終耗盡系統(tǒng)的所有內(nèi)存。所以,我們稱這類內(nèi)存泄漏為隱式內(nèi)存泄漏。舉一個例子:
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端斷開后,Server并沒有呼叫OnClientDisconnected()函數(shù),那么代表那次連接的Connection對象就不會被及時的刪除(在Server程序退出的時候,所有Connection對象會在ConnectionManager的析構(gòu)函數(shù)里被刪除)。當(dāng)不斷的有連接建立、斷開時隱式內(nèi)存泄漏就發(fā)生了。
從用戶使用程序的角度來看,內(nèi)存泄漏本身不會產(chǎn)生什么危害,作為一般的用戶,根本感覺不到內(nèi)存泄漏的存在。真正有危害的是內(nèi)存泄漏的堆積,這會最終消耗盡系統(tǒng)所有的內(nèi)存。從這個角度來說,一次性內(nèi)存泄漏并沒有什么危害,因?yàn)樗粫逊e,而隱式內(nèi)存泄漏危害性則非常大,因?yàn)檩^之于常發(fā)性和偶發(fā)性內(nèi)存泄漏它更難被檢測到。
2.3?檢測內(nèi)存泄漏
檢測內(nèi)存泄漏的關(guān)鍵是要能截獲住對分配內(nèi)存和釋放內(nèi)存的函數(shù)的調(diào)用。截獲住這兩個函數(shù),我們就能跟蹤每一塊內(nèi)存的生命周期,比如,每當(dāng)成功的分配一塊內(nèi)存后,就把它的指針加入一個全局的list中;每當(dāng)釋放一塊內(nèi)存,再把它的指針從list中刪除。這樣,當(dāng)程序結(jié)束的時候,list中剩余的指針就是指向那些沒有被釋放的內(nèi)存。文章來源:http://www.zghlxwxcb.cn/news/detail-404762.html
如果要檢測堆內(nèi)存的泄漏,那么需要截獲住malloc/realloc/free和new/delete就可以了(其實(shí)new/delete最終也是用malloc/free的,所以只要截獲前面一組即可)。對于其他的泄漏,可以采用類似的方法,截獲住相應(yīng)的分配和釋放函數(shù)。文章來源地址http://www.zghlxwxcb.cn/news/detail-404762.html
到了這里,關(guān)于深入理解C++內(nèi)存管理的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!