C/C++内存管理详解
內(nèi)存管理是C++最令人切齒痛恨的問題,也是C++最有爭議的問題,C++高手從中獲得了更好的性能,更大的自由,C++菜鳥的收獲則是一遍一遍的檢查代碼和對C++的痛恨,但內(nèi)存管理在C++中無處不在,內(nèi)存泄漏幾乎在每個(gè)C++程序中都會(huì)發(fā)生,因此要想成為C++高手,內(nèi)存管理一關(guān)是必須要過的,除非放棄C++,轉(zhuǎn)到Java或者.NET,他們的內(nèi)存管理基本是自動(dòng)的,當(dāng)然你也放棄了自由和對內(nèi)存的支配權(quán),還放棄了C++超絕的性能。
偉大的Bill Gates 曾經(jīng)失言:
640K ought to be enough for everybody?— Bill Gates 1981
程序員們經(jīng)常編寫內(nèi)存管理程序,往往提心吊膽。如果不想觸雷,唯一的解決辦法就是發(fā)現(xiàn)所有潛伏的地雷并且排除它們,躲是躲不了的。
?在C++中,內(nèi)存分成5個(gè)區(qū),他們分別是堆、棧、自由存儲(chǔ)區(qū)、全局/靜態(tài)存儲(chǔ)區(qū)和常量存儲(chǔ)區(qū)。
棧:在執(zhí)行函數(shù)時(shí),函數(shù)內(nèi)局部變量的存儲(chǔ)單元都可以在棧上創(chuàng)建,函數(shù)執(zhí)行結(jié)束時(shí)這些存儲(chǔ)單元自動(dòng)被釋放。棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。
堆:就是那些由?new分配的內(nèi)存塊,他們的釋放編譯器不去管,由我們的應(yīng)用程序去控制,一般一個(gè)new就要對應(yīng)一個(gè)?delete。如果程序員沒有釋放掉,那么在程序結(jié)束后,操作系統(tǒng)會(huì)自動(dòng)回收。
自由存儲(chǔ)區(qū):就是那些由malloc等分配的內(nèi)存塊,他和堆是十分相似的,不過它是用free來結(jié)束自己的生命的。
全局/靜態(tài)存儲(chǔ)區(qū):全局變量和靜態(tài)變量被分配到同一塊內(nèi)存中,在以前的C語言中,全局變量又分為初始化的和未初始化的,在C++里面沒有這個(gè)區(qū)分了,他們共同占用同一塊內(nèi)存區(qū)。
常量存儲(chǔ)區(qū):這是一塊比較特殊的存儲(chǔ)區(qū),他們里面存放的是常量,不允許修改。
明確區(qū)分堆與棧
堆與棧的區(qū)分問題,似乎是一個(gè)永恒的話題,由此可見,初學(xué)者對此往往是混淆不清的,所以我決定拿他第一個(gè)開刀。
首先,我們舉一個(gè)例子:
| 1 | void f() { int* p=new int[5]; } |
這條短短的一句話就包含了堆與棧,看到new,我們首先就應(yīng)該想到,我們分配了一塊堆內(nèi)存,那么指針p呢?他分配的是一塊棧內(nèi)存,所以這句話的意思就是:在棧內(nèi)存中存放了一個(gè)指向一塊堆內(nèi)存的指針p。在程序會(huì)先確定在堆中分配內(nèi)存的大小,然后調(diào)用operator new分配內(nèi)存,然后返回這塊內(nèi)存的首地址,放入棧中,他在VC6下的匯編代碼如下:
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么?澳,錯(cuò)了,應(yīng)該是delete []p,這是為了告訴編譯器:我刪除的是一個(gè)數(shù)組,編譯器就會(huì)根據(jù)相應(yīng)的Cookie信息去進(jìn)行釋放內(nèi)存的工作。
堆和棧究竟有什么區(qū)別
好了,我們回到我們的主題:堆和棧究竟有什么區(qū)別?
主要的區(qū)別由以下幾點(diǎn):
(1). 管理方式不同
(2). 空間大小不同
(3). 能否產(chǎn)生碎片不同
(4). 生長方向不同
(5). 分配方式不同
(6). 分配效率不同
管理方式:對于棧來講,是由編譯器自動(dòng)管理,無需我們手工控制;對于堆來說,釋放工作由程序員控制,容易產(chǎn)生memory leak。
空間大小:一般來講在32位系統(tǒng)下,堆內(nèi)存可以達(dá)到4G的空間,從這個(gè)角度來看堆內(nèi)存幾乎是沒有什么限制的。但是對于棧來講,一般都是有一定的空間大小的,例如,在VC6下面,默認(rèn)的??臻g大小是1M(好像是,記不清楚了)。當(dāng)然,我們可以修改:
打開工程,依次操作菜單如下:Project->Setting->Link,在Category?中選中Output,然后在Reserve中設(shè)定堆棧的最大值和commit。
注意:reserve最小值為4Byte;commit是保留在虛擬內(nèi)存的頁文件里面,它設(shè)置的較大會(huì)使棧開辟較大的值,可能增加內(nèi)存的開銷和啟動(dòng)時(shí)間。
碎片問題:對于堆來講,頻繁的new/delete勢必會(huì)造成內(nèi)存空間的不連續(xù),從而造成大量的碎片,使程序效率降低。對于棧來講,則不會(huì)存在這個(gè)問題,因?yàn)闂J窍冗M(jìn)后出的隊(duì)列,他們是如此的一一對應(yīng),以至于永遠(yuǎn)都不可能有一個(gè)內(nèi)存塊從棧中間彈出,在他彈出之前,在他上面的后進(jìn)的棧內(nèi)容已經(jīng)被彈出,詳細(xì)的可以參考數(shù)據(jù)結(jié)構(gòu),這里我們就不再一一討論了。
生長方向:對于堆來講,生長方向是向上的,也就是向著內(nèi)存地址增加的方向;對于棧來講,它的生長方向是向下的,是向著內(nèi)存地址減小的方向增長。
分配方式:堆都是動(dòng)態(tài)分配的,沒有靜態(tài)分配的堆。棧有2種分配方式:靜態(tài)分配和動(dòng)態(tài)分配。靜態(tài)分配是編譯器完成的,比如局部變量的分配。動(dòng)態(tài)分配由alloca函數(shù)進(jìn)行分配,但是棧的動(dòng)態(tài)分配和堆是不同的,他的動(dòng)態(tài)分配是由編譯器進(jìn)行釋放,無需我們手工實(shí)現(xiàn)。
分配效率:棧是機(jī)器系統(tǒng)提供的數(shù)據(jù)結(jié)構(gòu),計(jì)算機(jī)會(huì)在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執(zhí)行,這就決定了棧的效率比較高。堆則是C/C++函數(shù)庫提供的,它的機(jī)制是很復(fù)雜的,例如為了分配一塊內(nèi)存,庫函數(shù)會(huì)按照一定的算法(具體的算法可以參考數(shù)據(jù)結(jié)構(gòu)/操作系統(tǒng))在堆內(nèi)存中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由于內(nèi)存碎片太多),就有可能調(diào)用系統(tǒng)功能去增加程序數(shù)據(jù)段的內(nèi)存空間,這樣就有機(jī)會(huì)分到足夠大小的內(nèi)存,然后進(jìn)行返回。顯然,堆的效率比棧要低得多。
從這里我們可以看到,堆和棧相比,由于大量new/delete的使用,容易造成大量的內(nèi)存碎片;由于沒有專門的系統(tǒng)支持,效率很低;由于可能引發(fā)用戶態(tài)和核心態(tài)的切換,內(nèi)存的申請,代價(jià)變得更加昂貴。所以棧在程序中是應(yīng)用最廣泛的,就算是函數(shù)的調(diào)用也利用棧去完成,函數(shù)調(diào)用過程中的參數(shù),返回地址,EBP和局部變量都采用棧的方式存放。所以,我們推薦大家盡量用棧,而不是用堆。
雖然棧有如此眾多的好處,但是由于和堆相比不是那么靈活,有時(shí)候分配大量的內(nèi)存空間,還是用堆好一些。
無論是堆還是棧,都要防止越界現(xiàn)象的發(fā)生(除非你是故意使其越界),因?yàn)樵浇绲慕Y(jié)果要么是程序崩潰,要么是摧毀程序的堆、棧結(jié)構(gòu),產(chǎn)生以想不到的結(jié)果,就算是在你的程序運(yùn)行過程中,沒有發(fā)生上面的問題,你還是要小心,說不定什么時(shí)候就崩掉,那時(shí)候debug可是相當(dāng)困難的:)
?
控制C++的內(nèi)存分配
在嵌入式系統(tǒng)中使用C++的一個(gè)常見問題是內(nèi)存分配,即對new?和?delete?操作符的失控。
具有諷刺意味的是,問題的根源卻是C++對內(nèi)存的管理非常的容易而且安全。具體地說,當(dāng)一個(gè)對象被消除時(shí),它的析構(gòu)函數(shù)能夠安全的釋放所分配的內(nèi)存。
這當(dāng)然是個(gè)好事情,但是這種使用的簡單性使得程序員們過度使用new?和?delete,而不注意在嵌入式C++環(huán)境中的因果關(guān)系。并且,在嵌入式系統(tǒng)中,由于內(nèi)存的限制,頻繁的動(dòng)態(tài)分配不定大小的內(nèi)存會(huì)引起很大的問題以及堆破碎的風(fēng)險(xiǎn)。
作為忠告,保守的使用內(nèi)存分配是嵌入式環(huán)境中的第一原則。
但當(dāng)你必須要使用new和delete時(shí),你不得不控制C++中的內(nèi)存分配。你需要用一個(gè)全局的new?和delete來代替系統(tǒng)的內(nèi)存分配符,并且一個(gè)類一個(gè)類的重載new和delete。
一個(gè)防止堆破碎的通用方法是從不同固定大小的內(nèi)存持中分配不同類型的對象。對每個(gè)類重載new?和delete就提供了這樣的控制。
重載全局的new和delete操作符
可以很容易地重載new 和 delete 操作符,如下所示:
| 1 2 3 4 5 6 7 8 | 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()。
也可以對單個(gè)類的new?和?delete操作符重載。這是你能靈活的控制對象的內(nèi)存分配。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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)存池中分配不同的類對象。
為單個(gè)的類重載new[]和delete[]
必須小心對象數(shù)組的分配。你可能希望調(diào)用到被你重載過的new?和?delete?操作符,但并不如此。內(nèi)存的請求被定向到全局的new[]和delete[]?操作符,而這些內(nèi)存來自于系統(tǒng)堆。
C++將對象數(shù)組的內(nèi)存分配作為一個(gè)單獨(dú)的操作,而不同于單個(gè)對象的內(nèi)存分配。為了改變這種方式,你同樣需要重載new[]?和?delete[]操作符。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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++的實(shí)現(xiàn),new[]操作符中的個(gè)數(shù)參數(shù)是數(shù)組的大小加上額外的存儲(chǔ)對象數(shù)目的一些字節(jié)。在你的內(nèi)存分配機(jī)制重要考慮的這一點(diǎn)。你應(yīng)該盡量避免分配對象數(shù)組,從而使你的內(nèi)存分配策略簡單。
常見的內(nèi)存錯(cuò)誤及其對策
發(fā)生內(nèi)存錯(cuò)誤是件非常麻煩的事情。編譯器不能自動(dòng)發(fā)現(xiàn)這些錯(cuò)誤,通常是在程序運(yùn)行時(shí)才能捕捉到。而這些錯(cuò)誤大多沒有明顯的癥狀,時(shí)隱時(shí)現(xiàn),增加了改錯(cuò)的難度。有時(shí)用戶怒氣沖沖地把你找來,程序卻沒有發(fā)生任何問題,你一走,錯(cuò)誤又發(fā)作了。 常見的內(nèi)存錯(cuò)誤及其對策如下:
- 內(nèi)存分配未成功,卻使用了它。編程新手常犯這種錯(cuò)誤,因?yàn)樗麄儧]有意識到內(nèi)存分配會(huì)不成功。常用解決辦法是,在使用內(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)行防錯(cuò)處理。
- 內(nèi)存分配雖然成功,但是尚未初始化就引用它。犯這種錯(cuò)誤主要有兩個(gè)起因:一是沒有初始化的觀念;二是誤以為內(nèi)存的缺省初值全為零,導(dǎo)致引用初值錯(cuò)誤(例如數(shù)組)。內(nèi)存的缺省初值究竟是什么并沒有統(tǒng)一的標(biāo)準(zhǔn),盡管有些時(shí)候?yàn)榱阒?#xff0c;我們寧可信其無不可信其有。所以無論用何種方式創(chuàng)建數(shù)組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。
- 內(nèi)存分配成功并且已經(jīng)初始化,但操作越過了內(nèi)存的邊界。例如在使用數(shù)組時(shí)經(jīng)常發(fā)生下標(biāo)“多1”或者“少1”的操作。特別是在for循環(huán)語句中,循環(huán)次數(shù)很容易搞錯(cuò),導(dǎo)致數(shù)組操作越界。
- 忘記了釋放內(nèi)存,造成內(nèi)存泄露。含有這種錯(cuò)誤的函數(shù)每被調(diào)用一次就丟失一塊內(nèi)存。剛開始時(shí)系統(tǒng)的內(nèi)存充足,你看不到錯(cuò)誤。終有一次程序突然死掉,系統(tǒng)出現(xiàn)提示:內(nèi)存耗盡。動(dòng)態(tài)內(nèi)存的申請與釋放必須配對,程序中malloc與free的使用次數(shù)一定要相同,否則肯定有錯(cuò)誤(new/delete同理)。
- 釋放了內(nèi)存卻繼續(xù)使用它。
有三種情況:
(1). 程序中的對象調(diào)用關(guān)系過于復(fù)雜,實(shí)在難以搞清楚某個(gè)對象究竟是否已經(jīng)釋放了內(nèi)存,此時(shí)應(yīng)該重新設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu),從根本上解決對象管理的混亂局面。
(2). 函數(shù)的return語句寫錯(cuò)了,注意不要返回指向“棧內(nèi)存”的“指針”或者“引用”,因?yàn)樵搩?nèi)存在函數(shù)體結(jié)束時(shí)被自動(dòng)銷毀。
(3). 使用free或delete釋放了內(nèi)存后,沒有將指針設(shè)置為NULL。導(dǎo)致產(chǎn)生“野指針”。
那么如何避免產(chǎn)生野指針呢?這里列出了5條規(guī)則,平常寫程序時(shí)多注意一下,養(yǎng)成良好的習(xí)慣。
規(guī)則1:用malloc或new申請內(nèi)存之后,應(yīng)該立即檢查指針值是否為NULL。防止使用指針值為NULL的內(nèi)存。
規(guī)則2:不要忘記為數(shù)組和動(dòng)態(tài)內(nèi)存賦初值。防止將未被初始化的內(nèi)存作為右值使用。
規(guī)則3:避免數(shù)組或指針的下標(biāo)越界,特別要當(dāng)心發(fā)生“多1”或者“少1”操作。
規(guī)則4:動(dòng)態(tài)內(nèi)存的申請與釋放必須配對,防止內(nèi)存泄漏。
規(guī)則5:用free或delete釋放了內(nèi)存之后,立即將指針設(shè)置為NULL,防止產(chǎn)生“野指針”。
?
?
?
?
?
?
針與數(shù)組的對比
C++/C程序中,指針和數(shù)組在不少地方可以相互替換著用,讓人產(chǎn)生一種錯(cuò)覺,以為兩者是等價(jià)的。
數(shù)組要么在靜態(tài)存儲(chǔ)區(qū)被創(chuàng)建(如全局?jǐn)?shù)組),要么在棧上被創(chuàng)建。數(shù)組名對應(yīng)著(而不是指向)一塊內(nèi)存,其地址與容量在生命期內(nèi)保持不變,只有數(shù)組的內(nèi)容可以改變。
指針可以隨時(shí)指向任意類型的內(nèi)存塊,它的特征是“可變”,所以我們常用指針來操作動(dòng)態(tài)內(nèi)存。指針遠(yuǎn)比數(shù)組靈活,但也更危險(xiǎn)。
下面以字符串為例比較指針與數(shù)組的特性。
修改內(nèi)容
下面示例中,字符數(shù)組a的容量是6個(gè)字符,其內(nèi)容為 hello。a的內(nèi)容可以改變,如a[0]= ‘X’。指針p指向常量字符串“world”(位于靜態(tài)存儲(chǔ)區(qū),內(nèi)容為world),常量字符串的內(nèi)容是不可以被修改的。從語法上看,編譯器并不覺得語句p[0]= ‘X’有什么不妥,但是該語句企圖修改常量字符串的內(nèi)容而導(dǎo)致運(yùn)行錯(cuò)誤。
| 1 2 3 4 5 6 | char a[] = “hello”; a[0] = ‘X’; cout << a << endl; char *p = “world”; // 注意p指向常量字符串 p[0] = ‘X’; // 編譯器不能發(fā)現(xiàn)該錯(cuò)誤 cout << p << endl; |
內(nèi)容復(fù)制與比較
不能對數(shù)組名進(jìn)行直接復(fù)制與比較。若想把數(shù)組a的內(nèi)容復(fù)制給數(shù)組b,不能用語句?b = a?,否則將產(chǎn)生編譯錯(cuò)誤。應(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(bǔ)的內(nèi)容復(fù)制指針p,而是把a(bǔ)的地址賦給了p。要想復(fù)制a的內(nèi)容,可以先用庫函數(shù)malloc為p申請一塊容量為strlen(a)+1個(gè)字符的內(nèi)存,再用strcpy進(jìn)行字符串復(fù)制。同理,語句if(p==a)?比較的不是內(nèi)容而是地址,應(yīng)該用庫函數(shù)strcmp來比較。
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | // 數(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) … |
?
計(jì)算內(nèi)存容量
用運(yùn)算符sizeof可以計(jì)算出數(shù)組的容量(字節(jié)數(shù))。如下示例中,sizeof(a)的值是12(注意別忘了’’)。指針p指向a,但是sizeof(p)的值卻是4。這是因?yàn)閟izeof(p)得到的是一個(gè)指針變量的字節(jié)數(shù),相當(dāng)于sizeof(char*),而不是p所指的內(nèi)存容量。C++/C語言沒有辦法知道指針?biāo)傅膬?nèi)存容量,除非在申請內(nèi)存時(shí)記住它。
| 1 2 3 4 | char a[] = "hello world"; char *p = a; cout<< sizeof(a) << endl; // 12字節(jié) cout<< sizeof(p) << endl; // 4字節(jié) |
注意當(dāng)數(shù)組作為函數(shù)的參數(shù)進(jìn)行傳遞時(shí),該數(shù)組自動(dòng)退化為同類型的指針。如下示例中,不論數(shù)組a的容量是多少,sizeof(a)始終等于sizeof(char *)。
| 1 2 3 | void Func(char a[100]){ cout<< sizeof(a) << endl; // 4字節(jié)而不是100字節(jié) } ? |
?
指針參數(shù)是如何傳遞內(nèi)存的
如果函數(shù)的參數(shù)是一個(gè)指針,不要指望用該指針去申請動(dòng)態(tài)內(nèi)存。如下示例中,Test函數(shù)的語句GetMemory(str, 200)并沒有使str獲得期望的內(nèi)存,str依舊是NULL,為什么?
| 1 2 3 4 5 6 7 8 9 10 | 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"); // 運(yùn)行錯(cuò)誤 } |
毛病出在函數(shù)GetMemory中。編譯器總是要為函數(shù)的每個(gè)參數(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并不能輸出任何東西。事實(shí)上,每執(zhí)行一次GetMemory就會(huì)泄露一塊內(nèi)存,因?yàn)闆]有用free釋放內(nèi)存。
如果非得要用指針參數(shù)去申請內(nèi)存,那么應(yīng)該改用“指向指針的指針”,見示例:
| 1 2 3 4 5 6 7 8 9 10 11 12 | void GetMemory2(char **p, int num){ *p = (char *)malloc(sizeof(char) * num); } void Test2(void){ char *str = NULL; GetMemory2(&str, 100); // 注意參數(shù)是 &str,而不是str strcpy(str, "hello"); cout<< str << endl; free(str); } |
由于“指向指針的指針”這個(gè)概念不容易理解,我們可以用函數(shù)返回值來傳遞動(dòng)態(tài)內(nèi)存。這種方法更加簡單,見示例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | char *GetMemory3(int num){ char *p = (char *)malloc(sizeof(char) * num); return p; } void Test3(void){ char *str = NULL; str = GetMemory3(100); strcpy(str, "hello"); cout<< str << endl; free(str); } |
用函數(shù)返回值來傳遞動(dòng)態(tài)內(nèi)存這種方法雖然好用,但是常常有人把return語句用錯(cuò)了。這里強(qiáng)調(diào)不要用return語句返回指向“棧內(nèi)存”的指針,因?yàn)樵搩?nèi)存在函數(shù)結(jié)束時(shí)自動(dòng)消亡,見示例:
| 1 2 3 4 5 6 7 8 9 10 | char *GetString(void){ char p[] = "hello world"; return p; // 編譯器將提出警告 } void Test4(void){ char *str = NULL; str = GetString(); // str 的內(nèi)容是垃圾 cout<< str << endl; } |
用調(diào)試器逐步跟蹤Test4,發(fā)現(xiàn)執(zhí)行str = GetString語句后str不再是NULL指針,但是str的內(nèi)容不是“hello world”而是垃圾。
如果把上述示例改寫成如下示例,會(huì)怎么樣?
| 1 2 3 4 5 6 7 8 9 10 | char *GetString2(void){ char *p = "hello world"; return p; } void Test5(void){ char *str = NULL; str = GetString2(); cout<< str << endl; } |
函數(shù)Test5運(yùn)行雖然不會(huì)出錯(cuò),但是函數(shù)GetString2的設(shè)計(jì)概念卻是錯(cuò)誤的。因?yàn)镚etString2內(nèi)的“hello world”是常量字符串,位于靜態(tài)存儲(chǔ)區(qū),它在程序生命期內(nèi)恒定不變。無論什么時(shí)候調(diào)用GetString2,它返回的始終是同一個(gè)“只讀”的內(nèi)存塊。
杜絕“野指針”
“野指針”不是NULL指針,是指向“垃圾”內(nèi)存的指針。人們一般不會(huì)錯(cuò)用NULL指針,因?yàn)橛胕f語句很容易判斷。但是“野指針”是很危險(xiǎn)的,if語句對它不起作用。 “野指針”的成因主要有三種:
(1). 指針變量沒有被初始化。任何指針變量剛被創(chuàng)建時(shí)不會(huì)自動(dòng)成為NULL指針,它的缺省值是隨機(jī)的,它會(huì)亂指一氣。所以,指針變量在創(chuàng)建的同時(shí)應(yīng)當(dāng)被初始化,要么將指針設(shè)置為NULL,要么讓它指向合法的內(nèi)存。例如:
| 1 2 | char *p = NULL; char *str = (char *) malloc(100); |
(2). 指針p被free或者delete之后,沒有置為NULL,讓人誤以為p是個(gè)合法的指針。
(3). 指針操作超越了變量的作用域范圍。這種情況讓人防不勝防,示例程序如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | class A{ public: void Func(void){ cout << “Func of class A” << endl; } }; void Test(void){ A *p; { A a; p = &a; // 注意 a 的生命期 } p->Func(); // p是“野指針” } |
函數(shù)Test在執(zhí)行語句p->Func()時(shí),對象a已經(jīng)消失,而p是指向a的,所以p就成了“野指針”。但奇怪的是我運(yùn)行這個(gè)程序時(shí)居然沒有出錯(cuò),這可能與編譯器有關(guān)。
?
有了malloc/free為什么還要new/delete
malloc與free是C++/C語言的標(biāo)準(zhǔn)庫函數(shù),new/delete是C++的運(yùn)算符。它們都可用于申請動(dòng)態(tài)內(nèi)存和釋放內(nèi)存。
對于非內(nèi)部數(shù)據(jù)類型的對象而言,光用maloc/free無法滿足動(dòng)態(tài)對象的要求。對象在創(chuàng)建的同時(shí)要自動(dòng)執(zhí)行構(gòu)造函數(shù),對象在消亡之前要自動(dòng)執(zhí)行析構(gòu)函數(shù)。由于malloc/free是庫函數(shù)而不是運(yùn)算符,不在編譯器控制權(quán)限之內(nèi),不能夠把執(zhí)行構(gòu)造函數(shù)和析構(gòu)函數(shù)的任務(wù)強(qiáng)加于malloc/free。
因此C++語言需要一個(gè)能完成動(dòng)態(tài)內(nèi)存分配和初始化工作的運(yùn)算符new,以及一個(gè)能完成清理與釋放內(nèi)存工作的運(yùn)算符delete。注意new/delete不是庫函數(shù)。我們先看一看malloc/free和new/delete如何實(shí)現(xiàn)對象的動(dòng)態(tài)內(nèi)存管理,見示例:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class Obj{ public : Obj(void){ cout << “Initialization” << endl; } ~Obj(void){ cout << “Destroy” << endl; } void Initialize(void){ cout << “Initialization” << endl; } void Destroy(void){ cout << “Destroy” << endl; } }; void UseMallocFree(void){ Obj *a = (obj *)malloc(sizeof(obj)); // 申請動(dòng)態(tài)內(nèi)存 a->Initialize(); // 初始化 //… a->Destroy(); // 清除工作 free(a); // 釋放內(nèi)存 } void UseNewDelete(void){ Obj *a = new Obj; // 申請動(dòng)態(tài)內(nèi)存并且初始化 //… delete a; // 清除并且釋放內(nèi)存 } |
類Obj的函數(shù)Initialize模擬了構(gòu)造函數(shù)的功能,函數(shù)Destroy模擬了析構(gòu)函數(shù)的功能。函數(shù)UseMallocFree中,由于malloc/free不能執(zhí)行構(gòu)造函數(shù)與析構(gòu)函數(shù),必須調(diào)用成員函數(shù)Initialize和Destroy來完成初始化與清除工作。函數(shù)UseNewDelete則簡單得多。
所以我們不要企圖用malloc/free來完成動(dòng)態(tài)對象的內(nèi)存管理,應(yīng)該用new/delete。由于內(nèi)部數(shù)據(jù)類型的“對象”沒有構(gòu)造與析構(gòu)的過程,對它們而言malloc/free和new/delete是等價(jià)的。
既然new/delete的功能完全覆蓋了malloc/free,為什么C++不把malloc/free淘汰出局呢?這是因?yàn)镃++程序經(jīng)常要調(diào)用C函數(shù),而C程序只能用malloc/free管理動(dòng)態(tài)內(nèi)存。
如果用free釋放“new創(chuàng)建的動(dòng)態(tài)對象”,那么該對象因無法執(zhí)行析構(gòu)函數(shù)而可能導(dǎo)致程序出錯(cuò)。如果用delete釋放“malloc申請的動(dòng)態(tài)內(nèi)存”,結(jié)果也會(huì)導(dǎo)致程序出錯(cuò),但是該程序的可讀性很差。所以new/delete必須配對使用,malloc/free也一樣。
?
內(nèi)存耗盡怎么辦
如果在申請動(dòng)態(tài)內(nèi)存時(shí)找不到足夠大的內(nèi)存塊,malloc和new將返回NULL指針,宣告內(nèi)存申請失敗。通常有三種方式處理“內(nèi)存耗盡”問題。
(1). 判斷指針是否為NULL,如果是則馬上用return語句終止本函數(shù)。例如:
| 1 2 3 4 5 6 | void Func(void){ A *a = new A; if(a == NULL) return; … } |
(2). 判斷指針是否為NULL,如果是則馬上用exit(1)終止整個(gè)程序的運(yùn)行。例如:
| 1 2 3 4 5 6 7 8 9 | void Func(void){ A *a = new A; if(a == NULL){ cout << “Memory Exhausted” << endl; exit(1); } … } |
(3). 為new和malloc設(shè)置異常處理函數(shù)。例如Visual C++可以用_set_new_hander函數(shù)為new設(shè)置用戶自己定義的異常處理函數(shù),也可以讓malloc享用與new相同的異常處理函數(shù)。詳細(xì)內(nèi)容請參考C++使用手冊。
上述 (1)、(2) 方式使用最普遍。如果一個(gè)函數(shù)內(nèi)有多處需要申請動(dòng)態(tài)內(nèi)存,那么方式 (1) 就顯得力不從心(釋放內(nèi)存很麻煩),應(yīng)該用方式 (2) 來處理。
很多人不忍心用exit(1),問:“不編寫出錯(cuò)處理程序,讓操作系統(tǒng)自己解決行不行?”
不行。如果發(fā)生“內(nèi)存耗盡”這樣的事情,一般說來應(yīng)用程序已經(jīng)無藥可救。如果不用exit(1)?把壞程序殺死,它可能會(huì)害死操作系統(tǒng)。道理如同:如果不把歹徒擊斃,歹徒在老死之前會(huì)犯下更多的罪。
有一個(gè)很重要的現(xiàn)象要告訴大家。對于32位以上的應(yīng)用程序而言,無論怎樣使用malloc與new,幾乎不可能導(dǎo)致“內(nèi)存耗盡”。對于32位以上的應(yīng)用程序,“內(nèi)存耗盡”錯(cuò)誤處理程序毫無用處。這下可把Unix和Windows程序員們樂壞了:反正錯(cuò)誤處理程序不起作用,我就不寫了,省了很多麻煩。
必須強(qiáng)調(diào):不加錯(cuò)誤處理將導(dǎo)致程序的質(zhì)量很差,千萬不可因小失大。
?
| 1 2 3 4 5 6 7 8 9 | void main(void){ float *p = NULL; while(TRUE){ p = new float[1000000]; cout << “eat memory” << endl; if(p==NULL) exit(1); } } ? |
?
malloc/free的使用要點(diǎn)
函數(shù)malloc的原型如下:
| 1 | void * malloc(size_t size); |
用malloc申請一塊長度為length的整數(shù)類型的內(nèi)存,程序如下:
| 1 | int *p = (int *) malloc(sizeof(int) * length); |
我們應(yīng)當(dāng)把注意力集中在兩個(gè)要素上:“類型轉(zhuǎn)換”和“sizeof”。
* malloc返回值的類型是void*,所以在調(diào)用malloc時(shí)要顯式地進(jìn)行類型轉(zhuǎn)換,將void *轉(zhuǎn)換成所需要的指針類型。
* malloc函數(shù)本身并不識別要申請的內(nèi)存是什么類型,它只關(guān)心內(nèi)存的總字節(jié)數(shù)。我們通常記不住int,?float等數(shù)據(jù)類型的變量的確切字節(jié)數(shù)。例如int變量在16位系統(tǒng)下是2個(gè)字節(jié),在32位下是4個(gè)字節(jié);而float變量在16位系統(tǒng)下是4個(gè)字節(jié),在32位下也是4個(gè)字節(jié)。最好用以下程序作一次測試:
| 1 2 3 4 5 6 7 8 | cout << sizeof(char) << endl; cout << sizeof(int) << endl; cout << sizeof(unsigned int) << endl; cout << sizeof(long) << endl; cout << sizeof(unsigned long) << endl; cout << sizeof(float) << endl; cout << sizeof(double) << endl; cout << sizeof(void *) << endl; |
在malloc的“()”中使用sizeof運(yùn)算符是良好的風(fēng)格,但要當(dāng)心有時(shí)我們會(huì)昏了頭,寫出?p = malloc(sizeof(p))這樣的程序來。
函數(shù)free的原型如下:
| 1 | void free( void * memblock ); |
為什么free函數(shù)不象malloc函數(shù)那樣復(fù)雜呢?這是因?yàn)橹羔榩的類型以及它所指的內(nèi)存的容量事先都是知道的,語句free(p)能正確地釋放內(nèi)存。如果p是NULL指針,那么free對p無論操作多少次都不會(huì)出問題。如果p不是NULL指針,那么free對p連續(xù)操作兩次就會(huì)導(dǎo)致程序運(yùn)行錯(cuò)誤。
?
new/delete的使用要點(diǎn)
運(yùn)算符new使用起來要比函數(shù)malloc簡單得多,例如:
| 1 2 | int *p1 = (int *)malloc(sizeof(int) * length); int *p2 = new int[length]; |
這是因?yàn)閚ew內(nèi)置了sizeof、類型轉(zhuǎn)換和類型安全檢查功能。對于非內(nèi)部數(shù)據(jù)類型的對象而言,new在創(chuàng)建動(dòng)態(tài)對象的同時(shí)完成了初始化工作。如果對象有多個(gè)構(gòu)造函數(shù),那么new的語句也可以有多種形式。例如:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Obj{ public : Obj(void); // 無參數(shù)的構(gòu)造函數(shù) Obj(int x); // 帶一個(gè)參數(shù)的構(gòu)造函數(shù) … } void Test(void){ Obj *a = new Obj; Obj *b = new Obj(1); // 初值為1 … delete a; delete b; } |
如果用new創(chuàng)建對象數(shù)組,那么只能使用對象的無參數(shù)構(gòu)造函數(shù)。例如:
| 1 | Obj *objects = new Obj[100]; // 創(chuàng)建100個(gè)動(dòng)態(tài)對象 |
不能寫成:
| 1 | Obj *objects = new Obj[100](1);// 創(chuàng)建100個(gè)動(dòng)態(tài)對象的同時(shí)賦初值1 |
在用delete釋放對象數(shù)組時(shí),留意不要丟了符號‘[]’。例如:
| 1 2 | delete []objects; // 正確的用法 delete objects; // 錯(cuò)誤的用法 |
后者有可能引起程序崩潰和內(nèi)存泄漏。
?
轉(zhuǎn)載于:https://www.cnblogs.com/EMH899/p/10802760.html
總結(jié)
以上是生活随笔為你收集整理的C/C++内存管理详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 广告小程序后端开发(4.导入地区数据,修
- 下一篇: 如何调用Altera FPGA的内嵌乘法