C ?內(nèi)存管理(建議收藏)
內(nèi)存管理是C 最令人切齒痛恨的問題,也是C 最有爭議的問題,C 高手從中獲得了更好的性能,更大的自由,C 菜鳥的收獲則是一遍一遍的檢查代碼和對C 的痛恨,但內(nèi)存管理在C 中無處不在,內(nèi)存泄漏幾乎在每個C 程序中都會發(fā)生,因此要想成為C 高手,內(nèi)存管理一關(guān)是必須要過的,除非放棄C ,轉(zhuǎn)到Java或者C#,他們的內(nèi)存管理基本是自動的,當(dāng)然你也放棄了自由和對內(nèi)存的支配權(quán),還放棄了C 超絕的性能。本期專題將從內(nèi)存管理、內(nèi)存泄漏、內(nèi)存回收這三個方面來探討C 內(nèi)存管理問題。
1. 內(nèi)存管理
偉大的Bill Gates 曾經(jīng)失言:640K ought to be enough for everybody — Bill Gates 1981程序員們經(jīng)常編寫內(nèi)存管理程序,往往提心吊膽。如果不想觸雷,唯一的解決辦法就是發(fā)現(xiàn)所有潛伏的地雷并且排除它們,躲是躲不了的。本文的內(nèi)容比一般教科書的要深入得多,讀者需細(xì)心閱讀,做到真正地通曉內(nèi)存管理。1.1 C 內(nèi)存管理詳解
1.1.1 內(nèi)存分配方式
1.1.1.1 分配方式簡介
在C 中,內(nèi)存分成5個區(qū),他們分別是棧、堆、自由存儲區(qū)、全局/靜態(tài)存儲區(qū)和常量存儲區(qū)。棧,在執(zhí)行函數(shù)時,函數(shù)內(nèi)局部變量的存儲單元都可以在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束時這些存儲單元自動被釋放。棧內(nèi)存分配運算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。堆,就是那些由new分配的內(nèi)存塊,他們的釋放編譯器不去管,由我們的應(yīng)用程序去控制,一般一個new就要對應(yīng)一個delete。如果程序員沒有釋放掉,那么在程序結(jié)束后,操作系統(tǒng)會自動回收。自由存儲區(qū),就是那些由malloc等分配的內(nèi)存塊,他和堆是十分相似的,不過它是用free來結(jié)束自己的生命的。全局/靜態(tài)存儲區(qū),全局變量和靜態(tài)變量被分配到同一塊內(nèi)存中,在以前的C語言中,全局變量又分為初始化的和未初始化的,在C 里面沒有這個區(qū)分了,他們共同占用同一塊內(nèi)存區(qū)。常量存儲區(qū),這是一塊比較特殊的存儲區(qū),他們里面存放的是常量,不允許修改。1.1.1.2 明確區(qū)分堆與棧
在bbs上,堆與棧的區(qū)分問題,似乎是一個永恒的話題,由此可見,初學(xué)者對此往往是混淆不清的,所以我決定拿他第一個開刀。首先,我們舉一個例子:void?f()?{?int*?p=new?int[5];?}
這條短短的一句話就包含了堆與棧,看到new,我們首先就應(yīng)該想到,我們分配了一塊堆內(nèi)存,那么指針p呢?他分配的是一塊棧內(nèi)存,所以這句話的意思就是:在棧內(nèi)存中存放了一個指向一塊堆內(nèi)存的指針p。在程序會先確定在堆中分配內(nèi)存的大小,然后調(diào)用operator new分配內(nèi)存,然后返回這塊內(nèi)存的首地址,放入棧中,他在VC下的匯編代碼如下:00401028?push?14h
0040102A?call?operator?new?(00401060)
0040102F?add?esp,4
00401032?mov?dword?ptr?[ebp-8],eax
00401035?mov?eax,dword?ptr?[ebp-8]
00401038?mov?dword?ptr?[ebp-4],eax
這里,我們?yōu)榱撕唵尾]有釋放內(nèi)存,那么該怎么去釋放呢?是delete p么?澳,錯了,應(yīng)該是delete []p,這是為了告訴編譯器:我刪除的是一個數(shù)組,VC就會根據(jù)相應(yīng)的Cookie信息去進(jìn)行釋放內(nèi)存的工作。1.1.1.3 堆和棧究竟有什么區(qū)別?
好了,我們回到我們的主題:堆和棧究竟有什么區(qū)別?主要的區(qū)別由以下幾點:1、管理方式不同;2、空間大小不同;3、能否產(chǎn)生碎片不同;4、生長方向不同;5、分配方式不同;6、分配效率不同;管理方式:對于棧來講,是由編譯器自動管理,無需我們手工控制;對于堆來說,釋放工作由程序員控制,容易產(chǎn)生memory leak。空間大小:一般來講在32位系統(tǒng)下,堆內(nèi)存可以達(dá)到4G的空間,從這個角度來看堆內(nèi)存幾乎是沒有什么限制的。但是對于棧來講,一般都是有一定的空間大小的,例如,在VC下面,默認(rèn)的棧空間大小是1M(好像是,記不清楚了)。當(dāng)然,我們可以修改:打開工程,依次操作菜單如下:Project->Setting->Link,在Category 中選中Output,然后在Reserve中設(shè)定堆棧的最大值和commit。注意:reserve最小值為4Byte;commit是保留在虛擬內(nèi)存的頁文件里面,它設(shè)置的較大會使棧開辟較大的值,可能增加內(nèi)存的開銷和啟動時間。碎片問題:對于堆來講,頻繁的new/delete勢必會造成內(nèi)存空間的不連續(xù),從而造成大量的碎片,使程序效率降低。對于棧來講,則不會存在這個問題,因為棧是先進(jìn)后出的隊列,他們是如此的一一對應(yīng),以至于永遠(yuǎn)都不可能有一個內(nèi)存塊從棧中間彈出,在他彈出之前,在他上面的后進(jìn)的棧內(nèi)容已經(jīng)被彈出,詳細(xì)的可以參考數(shù)據(jù)結(jié)構(gòu),這里我們就不再一一討論了。生長方向:對于堆來講,生長方向是向上的,也就是向著內(nèi)存地址增加的方向;對于棧來講,它的生長方向是向下的,是向著內(nèi)存地址減小的方向增長。分配方式:堆都是動態(tài)分配的,沒有靜態(tài)分配的堆。棧有2種分配方式:靜態(tài)分配和動態(tài)分配。靜態(tài)分配是編譯器完成的,比如局部變量的分配。動態(tài)分配由alloca函數(shù)進(jìn)行分配,但是棧的動態(tài)分配和堆是不同的,他的動態(tài)分配是由編譯器進(jìn)行釋放,無需我們手工實現(xiàn)。分配效率:棧是機器系統(tǒng)提供的數(shù)據(jù)結(jié)構(gòu),計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執(zhí)行,這就決定了棧的效率比較高。堆則是C/C 函數(shù)庫提供的,它的機制是很復(fù)雜的,例如為了分配一塊內(nèi)存,庫函數(shù)會按照一定的算法(具體的算法可以參考數(shù)據(jù)結(jié)構(gòu)/操作系統(tǒng))在堆內(nèi)存中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由于內(nèi)存碎片太多),就有可能調(diào)用系統(tǒng)功能去增加程序數(shù)據(jù)段的內(nèi)存空間,這樣就有機會分到足夠大小的內(nèi)存,然后進(jìn)行返回。顯然,堆的效率比棧要低得多。從這里我們可以看到,堆和棧相比,由于大量new/delete的使用,容易造成大量的內(nèi)存碎片;由于沒有專門的系統(tǒng)支持,效率很低;由于可能引發(fā)用戶態(tài)和核心態(tài)的切換,內(nèi)存的申請,代價變得更加昂貴。所以棧在程序中是應(yīng)用最廣泛的,就算是函數(shù)的調(diào)用也利用棧去完成,函數(shù)調(diào)用過程中的參數(shù),返回地址,EBP和局部變量都采用棧的方式存放。所以,我們推薦大家盡量用棧,而不是用堆。雖然棧有如此眾多的好處,但是由于和堆相比不是那么靈活,有時候分配大量的內(nèi)存空間,還是用堆好一些。無論是堆還是棧,都要防止越界現(xiàn)象的發(fā)生(除非你是故意使其越界),因為越界的結(jié)果要么是程序崩潰,要么是摧毀程序的堆、棧結(jié)構(gòu),產(chǎn)生以想不到的結(jié)果,就算是在你的程序運行過程中,沒有發(fā)生上面的問題,你還是要小心,說不定什么時候就崩掉,那時候debug可是相當(dāng)困難的:)1.1.2 控制C 的內(nèi)存分配
在嵌入式系統(tǒng)中使用C 的一個常見問題是內(nèi)存分配,即對new 和 delete 操作符的失控。具有諷刺意味的是,問題的根源卻是C 對內(nèi)存的管理非常的容易而且安全。具體地說,當(dāng)一個對象被消除時,它的析構(gòu)函數(shù)能夠安全的釋放所分配的內(nèi)存。這當(dāng)然是個好事情,但是這種使用的簡單性使得程序員們過度使用new 和 delete,而不注意在嵌入式C 環(huán)境中的因果關(guān)系。并且,在嵌入式系統(tǒng)中,由于內(nèi)存的限制,頻繁的動態(tài)分配不定大小的內(nèi)存會引起很大的問題以及堆破碎的風(fēng)險。作為忠告,保守的使用內(nèi)存分配是嵌入式環(huán)境中的第一原則。但當(dāng)你必須要使用new 和delete時,你不得不控制C 中的內(nèi)存分配。你需要用一個全局的new 和delete來代替系統(tǒng)的內(nèi)存分配符,并且一個類一個類的重載new 和delete。一個防止堆破碎的通用方法是從不同固定大小的內(nèi)存持中分配不同類型的對象。對每個類重載new 和delete就提供了這樣的控制。1.1.2.1 重載全局的new和delete操作符
可以很容易地重載new 和 delete 操作符,如下所示:void?*?operator?new(size_t?size)
{
??void?*p?=?malloc(size);
??return?(p);
}
void?operator?delete(void?*p);
{
??free(p);
}
這段代碼可以代替默認(rèn)的操作符來滿足內(nèi)存分配的請求。出于解釋C 的目的,我們也可以直接調(diào)用malloc() 和free()。也可以對單個類的new 和 delete 操作符重載。這是你能靈活的控制對象的內(nèi)存分配。class?TestClass?{
??public:
??void?*?operator?new(size_t?size);
??void?operator?delete(void?*p);
????//?..?other?members?here?...
};
void?*TestClass::operator?new(size_t?size)
{
??void?*p?=?malloc(size);?//?Replace?this?with?alternative?allocator
??return?(p);
}
void?TestClass::operator?delete(void?*p)
{
??free(p);?//?Replace?this?with?alternative?de-allocator
}
所有TestClass 對象的內(nèi)存分配都采用這段代碼。更進(jìn)一步,任何從TestClass 繼承的類也都采用這一方式,除非它自己也重載了new 和 delete 操作符。通過重載new 和 delete 操作符的方法,你可以自由地采用不同的分配策略,從不同的內(nèi)存池中分配不同的類對象。1.1.2.2 為單個的類重載 new[ ]和delete[ ]
必須小心對象數(shù)組的分配。你可能希望調(diào)用到被你重載過的new 和 delete 操作符,但并不如此。內(nèi)存的請求被定向到全局的new[ ]和delete[ ] 操作符,而這些內(nèi)存來自于系統(tǒng)堆。C 將對象數(shù)組的內(nèi)存分配作為一個單獨的操作,而不同于單個對象的內(nèi)存分配。為了改變這種方式,你同樣需要重載new[ ] 和 delete[ ]操作符。class?TestClass?{
??public:
????void?*?operator?new[?](size_t?size);
????void?operator?delete[?](void?*p);
????//?..?other?members?here?..
};
void?*TestClass::operator?new[?](size_t?size)
{
??void?*p?=?malloc(size);
??return?(p);
}
void?TestClass::operator?delete[?](void?*p)
{
??free(p);
}
int?main(void)
{
??TestClass?*p?=?new?TestClass[10];
??//?...?etc?...
??delete[?]?p;
}
但是注意:對于多數(shù)C 的實現(xiàn),new[]操作符中的個數(shù)參數(shù)是數(shù)組的大小加上額外的存儲對象數(shù)目的一些字節(jié)。在你的內(nèi)存分配機制重要考慮的這一點。你應(yīng)該盡量避免分配對象數(shù)組,從而使你的內(nèi)存分配策略簡單。1.1.3 常見的內(nèi)存錯誤及其對策
發(fā)生內(nèi)存錯誤是件非常麻煩的事情。編譯器不能自動發(fā)現(xiàn)這些錯誤,通常是在程序運行時才能捕捉到。而這些錯誤大多沒有明顯的癥狀,時隱時現(xiàn),增加了改錯的難度。有時用戶怒氣沖沖地把你找來,程序卻沒有發(fā)生任何問題,你一走,錯誤又發(fā)作了。 常見的內(nèi)存錯誤及其對策如下:* 內(nèi)存分配未成功,卻使用了它。編程新手常犯這種錯誤,因為他們沒有意識到內(nèi)存分配會不成功。常用解決辦法是,在使用內(nèi)存之前檢查指針是否為NULL。如果指針p是函數(shù)的參數(shù),那么在函數(shù)的入口處用assert(p!=NULL)進(jìn)行檢查。如果是用malloc或new來申請內(nèi)存,應(yīng)該用if(p==NULL) 或if(p!=NULL)進(jìn)行防錯處理。* 內(nèi)存分配雖然成功,但是尚未初始化就引用它。犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為內(nèi)存的缺省初值全為零,導(dǎo)致引用初值錯誤(例如數(shù)組)。 內(nèi)存的缺省初值究竟是什么并沒有統(tǒng)一的標(biāo)準(zhǔn),盡管有些時候為零值,我們寧可信其無不可信其有。所以無論用何種方式創(chuàng)建數(shù)組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。* 內(nèi)存分配成功并且已經(jīng)初始化,但操作越過了內(nèi)存的邊界。例如在使用數(shù)組時經(jīng)常發(fā)生下標(biāo)“多1”或者“少1”的操作。特別是在for循環(huán)語句中,循環(huán)次數(shù)很容易搞錯,導(dǎo)致數(shù)組操作越界。* 忘記了釋放內(nèi)存,造成內(nèi)存泄露。含有這種錯誤的函數(shù)每被調(diào)用一次就丟失一塊內(nèi)存。剛開始時系統(tǒng)的內(nèi)存充足,你看不到錯誤。終有一次程序突然死掉,系統(tǒng)出現(xiàn)提示:內(nèi)存耗盡。動態(tài)內(nèi)存的申請與釋放必須配對,程序中malloc與free的使用次數(shù)一定要相同,否則肯定有錯誤(new/delete同理)。* 釋放了內(nèi)存卻繼續(xù)使用它。有三種情況:(1)程序中的對象調(diào)用關(guān)系過于復(fù)雜,實在難以搞清楚某個對象究竟是否已經(jīng)釋放了內(nèi)存,此時應(yīng)該重新設(shè)計數(shù)據(jù)結(jié)構(gòu),從根本上解決對象管理的混亂局面。(2)函數(shù)的return語句寫錯了,注意不要返回指向“棧內(nèi)存”的“指針”或者“引用”,因為該內(nèi)存在函數(shù)體結(jié)束時被自動銷毀。(3)使用free或delete釋放了內(nèi)存后,沒有將指針設(shè)置為NULL。導(dǎo)致產(chǎn)生“野指針”。【規(guī)則1】用malloc或new申請內(nèi)存之后,應(yīng)該立即檢查指針值是否為NULL。防止使用指針值為NULL的內(nèi)存。【規(guī)則2】不要忘記為數(shù)組和動態(tài)內(nèi)存賦初值。防止將未被初始化的內(nèi)存作為右值使用。【規(guī)則3】避免數(shù)組或指針的下標(biāo)越界,特別要當(dāng)心發(fā)生“多1”或者“少1”操作。【規(guī)則4】動態(tài)內(nèi)存的申請與釋放必須配對,防止內(nèi)存泄漏。【規(guī)則5】用free或delete釋放了內(nèi)存之后,立即將指針設(shè)置為NULL,防止產(chǎn)生“野指針”。1.1.4 指針與數(shù)組的對比
C /C程序中,指針和數(shù)組在不少地方可以相互替換著用,讓人產(chǎn)生一種錯覺,以為兩者是等價的。數(shù)組要么在靜態(tài)存儲區(qū)被創(chuàng)建(如全局?jǐn)?shù)組),要么在棧上被創(chuàng)建。數(shù)組名對應(yīng)著(而不是指向)一塊內(nèi)存,其地址與容量在生命期內(nèi)保持不變,只有數(shù)組的內(nèi)容可以改變。指針可以隨時指向任意類型的內(nèi)存塊,它的特征是“可變”,所以我們常用指針來操作動態(tài)內(nèi)存。指針遠(yuǎn)比數(shù)組靈活,但也更危險。下面以字符串為例比較指針與數(shù)組的特性。1.1.4.1 修改內(nèi)容
下面示例中,字符數(shù)組a的容量是6個字符,其內(nèi)容為hello。a的內(nèi)容可以改變,如a[0]= ‘X’。指針p指向常量字符串“world”(位于靜態(tài)存儲區(qū),內(nèi)容為world),常量字符串的內(nèi)容是不可以被修改的。從語法上看,編譯器并不覺得語句p[0]= ‘X’有什么不妥,但是該語句企圖修改常量字符串的內(nèi)容而導(dǎo)致運行錯誤。char?a[]?=?“hello”;
a[0]?=?‘X’;
cout?<char?*p?=?“world”;?//?注意p指向常量字符串
p[0]?=?‘X’;?//?編譯器不能發(fā)現(xiàn)該錯誤
cout?<
1.1.4.2 內(nèi)容復(fù)制與比較
不能對數(shù)組名進(jìn)行直接復(fù)制與比較。若想把數(shù)組a的內(nèi)容復(fù)制給數(shù)組b,不能用語句 b = a ,否則將產(chǎn)生編譯錯誤。應(yīng)該用標(biāo)準(zhǔn)庫函數(shù)strcpy進(jìn)行復(fù)制。同理,比較b和a的內(nèi)容是否相同,不能用if(b==a) 來判斷,應(yīng)該用標(biāo)準(zhǔn)庫函數(shù)strcmp進(jìn)行比較。語句p = a 并不能把a的內(nèi)容復(fù)制指針p,而是把a的地址賦給了p。要想復(fù)制a的內(nèi)容,可以先用庫函數(shù)malloc為p申請一塊容量為strlen(a) 1個字符的內(nèi)存,再用strcpy進(jìn)行字符串復(fù)制。同理,語句if(p==a) 比較的不是內(nèi)容而是地址,應(yīng)該用庫函數(shù)strcmp來比較。//?數(shù)組…
char?a[]?=?"hello";
char?b[10];
strcpy(b,?a);?//?不能用?b?=?a;
if(strcmp(b,?a)?==?0)?//?不能用?if?(b?==?a)
…
//?指針…
int?len?=?strlen(a);
char?*p?=?(char?*)malloc(sizeof(char)*(len 1));
strcpy(p,a);?//?不要用?p?=?a;
if(strcmp(p,?a)?==?0)?//?不要用?if?(p?==?a)
…
1.1.4.3 計算內(nèi)存容量
用運算符sizeof可以計算出數(shù)組的容量(字節(jié)數(shù))。如下示例中,sizeof(a)的值是12(注意別忘了’’)。指針p指向a,但是sizeof(p)的值卻是4。這是因為sizeof(p)得到的是一個指針變量的字節(jié)數(shù),相當(dāng)于sizeof(char*),而不是p所指的內(nèi)存容量。C /C語言沒有辦法知道指針?biāo)傅膬?nèi)存容量,除非在申請內(nèi)存時記住它。char?a[]?=?"hello?world";
char?*p?=?a;
cout<cout<
注意當(dāng)數(shù)組作為函數(shù)的參數(shù)進(jìn)行傳遞時,該數(shù)組自動退化為同類型的指針。如下示例中,不論數(shù)組a的容量是多少,sizeof(a)始終等于sizeof(char *)。void?Func(char?a[100]){?cout<
1.1.5 指針參數(shù)是如何傳遞內(nèi)存的?
如果函數(shù)的參數(shù)是一個指針,不要指望用該指針去申請動態(tài)內(nèi)存。如下示例中,Test函數(shù)的語句GetMemory(str, 200)并沒有使str獲得期望的內(nèi)存,str依舊是NULL,為什么?void?GetMemory(char?*p,?int?num){?p?=?(char?*)malloc(sizeof(char)?*?num);}void?Test(void){?char?*str?=?NULL;?GetMemory(str,?100);?//?str?仍然為?NULL?strcpy(str,?"hello");?//?運行錯誤}
毛病出在函數(shù)GetMemory中。編譯器總是要為函數(shù)的每個參數(shù)制作臨時副本,指針參數(shù)p的副本是 _p,編譯器使 _p = p。如果函數(shù)體內(nèi)的程序修改了_p的內(nèi)容,就導(dǎo)致參數(shù)p的內(nèi)容作相應(yīng)的修改。這就是指針可以用作輸出參數(shù)的原因。在本例中,_p申請了新的內(nèi)存,只是把_p所指的內(nèi)存地址改變了,但是p絲毫未變。所以函數(shù)GetMemory并不能輸出任何東西。事實上,每執(zhí)行一次GetMemory就會泄露一塊內(nèi)存,因為沒有用free釋放內(nèi)存。如果非得要用指針參數(shù)去申請內(nèi)存,那么應(yīng)該改用“指向指針的指針”,見示例:void?GetMemory2(char?**p,?int?num)
{
?*p?=?(char?*)malloc(sizeof(char)?*?num);
}
void?Test2(void)
{
?char?*str?=?NULL;
?GetMemory2(