C++內(nèi)存管理
[導(dǎo)語]
內(nèi)存管理是C++最令人切齒痛恨的問題,,也是C++最有爭議的問題,,C++高手從中獲得了更好的性能,更大的自由,,C++菜鳥的收獲則是一遍一遍的檢查代碼和對(duì)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)然你也放棄了自由和對(duì)內(nèi)存的支配權(quán),,還放棄了C++超絕的性能,。本期專題將從內(nèi)存管理、內(nèi)存泄漏,、內(nèi)存回收這三個(gè)方面來探討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個(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就要對(duì)應(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ū),他們里面存放的是常量,,不允許修改,。
1.1.1.2 明確區(qū)分堆與棧
在bbs上,堆與棧的區(qū)分問題,,似乎是一個(gè)永恒的話題,,由此可見,初學(xué)者對(duì)此往往是混淆不清的,,所以我決定拿他第一個(gè)開刀,。
首先,我們舉一個(gè)例子:
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ù)組,,VC6就會(huì)根據(jù)相應(yīng)的Cookie信息去進(jìn)行釋放內(nèi)存的工作,。
1.1.1.3 堆和棧究竟有什么區(qū)別?
好了,,我們回到我們的主題:堆和棧究竟有什么區(qū)別,?
主要的區(qū)別由以下幾點(diǎn):
1、管理方式不同,;
2,、空間大小不同;
3,、能否產(chǎn)生碎片不同,;
4、生長方向不同,;
5,、分配方式不同;
6,、分配效率不同,;
管理方式:對(duì)于棧來講,是由編譯器自動(dòng)管理,,無需我們手工控制,;對(duì)于堆來說,釋放工作由程序員控制,,容易產(chǎn)生memory leak,。
空間大小:一般來講在32位系統(tǒng)下,,堆內(nèi)存可以達(dá)到4G的空間,,從這個(gè)角度來看堆內(nèi)存幾乎是沒有什么限制的。但是對(duì)于棧來講,,一般都是有一定的空間大小的,,例如,在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í)間。
碎片問題:對(duì)于堆來講,,頻繁的new/delete勢(shì)必會(huì)造成內(nèi)存空間的不連續(xù),,從而造成大量的碎片,使程序效率降低,。對(duì)于棧來講,,則不會(huì)存在這個(gè)問題,因?yàn)闂J窍冗M(jìn)后出的隊(duì)列,,他們是如此的一一對(duì)應(yīng),,以至于永遠(yuǎn)都不可能有一個(gè)內(nèi)存塊從棧中間彈出,在他彈出之前,,在他上面的后進(jìn)的棧內(nèi)容已經(jīng)被彈出,,詳細(xì)的可以參考數(shù)據(jù)結(jié)構(gòu),這里我們就不再一一討論了,。
生長方向:對(duì)于堆來講,,生長方向是向上的,也就是向著內(nèi)存地址增加的方向,;對(duì)于棧來講,,它的生長方向是向下的,是向著內(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ì)在底層對(duì)棧提供支持:分配專門的寄存器存放棧的地址,,壓棧出棧都有專門的指令執(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)存的申請(qǐng),,代價(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)困難的:)
1.1.2 控制C++的內(nèi)存分配
在嵌入式系統(tǒng)中使用C++的一個(gè)常見問題是內(nèi)存分配,即對(duì)new 和 delete 操作符的失控,。
具有諷刺意味的是,,問題的根源卻是C++對(duì)內(nèi)存的管理非常的容易而且安全。具體地說,,當(dāng)一個(gè)對(duì)象被消除時(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)存持中分配不同類型的對(duì)象,。對(duì)每個(gè)類重載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)存分配的請(qǐng)求,。出于解釋C++的目的,我們也可以直接調(diào)用malloc() 和free(),。
也可以對(duì)單個(gè)類的new 和 delete 操作符重載,。這是你能靈活的控制對(duì)象的內(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 對(duì)象的內(nèi)存分配都采用這段代碼,。更進(jìn)一步,,任何從TestClass 繼承的類也都采用這一方式,除非它自己也重載了new 和 delete 操作符,。通過重載new 和 delete 操作符的方法,,你可以自由地采用不同的分配策略,從不同的內(nèi)存池中分配不同的類對(duì)象。
1.1.2.2 為單個(gè)的類重載 new[ ]和delete[ ]
必須小心對(duì)象數(shù)組的分配,。你可能希望調(diào)用到被你重載過的new 和 delete 操作符,,但并不如此。內(nèi)存的請(qǐng)求被定向到全局的new[ ]和delete[ ] 操作符,,而這些內(nèi)存來自于系統(tǒng)堆,。
C++將對(duì)象數(shù)組的內(nèi)存分配作為一個(gè)單獨(dú)的操作,而不同于單個(gè)對(duì)象的內(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;
}
但是注意:對(duì)于多數(shù)C++的實(shí)現(xiàn),,new[]操作符中的個(gè)數(shù)參數(shù)是數(shù)組的大小加上額外的存儲(chǔ)對(duì)象數(shù)目的一些字節(jié),。在你的內(nèi)存分配機(jī)制重要考慮的這一點(diǎn)。你應(yīng)該盡量避免分配對(duì)象數(shù)組,,從而使你的內(nèi)存分配策略簡單,。
1.1.3 常見的內(nèi)存錯(cuò)誤及其對(duì)策
發(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ò)誤及其對(duì)策如下:
* 內(nèi)存分配未成功,,卻使用了它。
編程新手常犯這種錯(cuò)誤,,因?yàn)樗麄儧]有意識(shí)到內(nèi)存分配會(huì)不成功,。常用解決辦法是,在使用內(nèi)存之前檢查指針是否為NULL,。如果指針p是函數(shù)的參數(shù),,那么在函數(shù)的入口處用assert(p!=NULL)進(jìn)行
檢查。如果是用malloc或new來申請(qǐng)內(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)榱阒?,我們寧可信其無不可信其有。所以無論用何種方式創(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)存的申請(qǐng)與釋放必須配對(duì),程序中malloc與free的使用次數(shù)一定要相同,,否則肯定有錯(cuò)誤(new/delete同理),。
* 釋放了內(nèi)存卻繼續(xù)使用它。
有三種情況:
?。?)程序中的對(duì)象調(diào)用關(guān)系過于復(fù)雜,,實(shí)在難以搞清楚某個(gè)對(duì)象究竟是否已經(jīng)釋放了內(nèi)存,此時(shí)應(yīng)該重新設(shè)計(jì)數(shù)據(jù)結(jié)構(gòu),,從根本上解決對(duì)象管理的混亂局面,。
(2)函數(shù)的return語句寫錯(cuò)了,,注意不要返回指向“棧內(nèi)存”的“指針”或者“引用”,,因?yàn)樵搩?nèi)存在函數(shù)體結(jié)束時(shí)被自動(dòng)銷毀。
?。?)使用free或delete釋放了內(nèi)存后,,沒有將指針設(shè)置為NULL。導(dǎo)致產(chǎn)生“野指針”,。
【規(guī)則1】用malloc或new申請(qǐng)內(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)存的申請(qǐng)與釋放必須配對(duì),,防止內(nèi)存泄漏。
【規(guī)則5】用free或delete釋放了內(nèi)存之后,,立即將指針設(shè)置為NULL,,防止產(chǎn)生“野指針”。
1.1.4 指針與數(shù)組的對(duì)比
C++/C程序中,,指針和數(shù)組在不少地方可以相互替換著用,,讓人產(chǎn)生一種錯(cuò)覺,以為兩者是等價(jià)的,。
數(shù)組要么在靜態(tài)存儲(chǔ)區(qū)被創(chuàng)建(如全局?jǐn)?shù)組),,要么在棧上被創(chuàng)建。數(shù)組名對(duì)應(yīng)著(而不是指向)一塊內(nèi)存,,其地址與容量在生命期內(nèi)保持不變,,只有數(shù)組的內(nèi)容可以改變。
指針可以隨時(shí)指向任意類型的內(nèi)存塊,,它的特征是“可變”,,所以我們常用指針來操作動(dòng)態(tài)內(nèi)存。指針遠(yuǎn)比數(shù)組靈活,,但也更危險(xiǎn)。
下面以字符串為例比較指針與數(shù)組的特性,。
1.1.4.1 修改內(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ò)誤。
char a[] = “hello”;
a[0] = ‘X’;
cout << a << endl;
char *p = “world”; // 注意p指向常量字符串
p[0] = ‘X’; // 編譯器不能發(fā)現(xiàn)該錯(cuò)誤
cout << p << endl;
1.1.4.2 內(nèi)容復(fù)制與比較
不能對(duì)數(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申請(qǐng)一塊容量為strlen(a)+1個(gè)字符的內(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 計(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)存容量,,除非在申請(qǐng)內(nèi)存時(shí)記住它,。
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 *),。
void Func(char a[100])
{
cout<< sizeof(a) << endl; // 4字節(jié)而不是100字節(jié)
}
1.1.5 指針參數(shù)是如何傳遞內(nèi)存的,?
如果函數(shù)的參數(shù)是一個(gè)指針,不要指望用該指針去申請(qǐng)動(dòng)態(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"); // 運(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申請(qǐng)了新的內(nèi)存,,只是把_p所指的內(nèi)存地址改變了,,但是p絲毫未變。所以函數(shù)GetMemory并不能輸出任何東西,。事實(shí)上,,每執(zhí)行一次GetMemory就會(huì)泄露一塊內(nèi)存,因?yàn)闆]有用free釋放內(nèi)存,。
如果非得要用指針參數(shù)去申請(qǐng)內(nèi)存,,那么應(yīng)該改用“指向指針的指針”,見示例:
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)存。這種方法更加簡單,,見示例:
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)消亡,,見示例:
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ì)怎么樣,?
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)存塊。
1.1.6 杜絕“野指針”
“野指針”不是NULL指針,,是指向“垃圾”內(nèi)存的指針,。人們一般不會(huì)錯(cuò)用NULL指針,因?yàn)橛胕f語句很容易判斷,。但是“野指針”是很危險(xiǎn)的,,if語句對(duì)它不起作用,。 “野指針”的成因主要有兩種:
(1)指針變量沒有被初始化。任何指針變量剛被創(chuàng)建時(shí)不會(huì)自動(dòng)成為NULL指針,,它的缺省值是隨機(jī)的,,它會(huì)亂指一氣。所以,,指針變量在創(chuàng)建的同時(shí)應(yīng)當(dāng)被初始化,,要么將指針設(shè)置為NULL,要么讓它指向合法的內(nèi)存,。例如
char *p = NULL;
char *str = (char *) malloc(100);
(2)指針p被free或者delete之后,,沒有置為NULL,讓人誤以為p是個(gè)合法的指針,。
(3)指針操作超越了變量的作用域范圍,。這種情況讓人防不勝防,示例程序如下:
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í),,對(duì)象a已經(jīng)消失,,而p是指向a的,所以p就成了“野指針”,。但奇怪的是我運(yùn)行這個(gè)程序時(shí)居然沒有出錯(cuò),,這可能與編譯器有關(guān)。
1.1.7 有了malloc/free為什么還要new/delete,?
malloc與free是C++/C語言的標(biāo)準(zhǔn)庫函數(shù),,new/delete是C++的運(yùn)算符。它們都可用于申請(qǐng)動(dòng)態(tài)內(nèi)存和釋放內(nèi)存,。
對(duì)于非內(nèi)部數(shù)據(jù)類型的對(duì)象而言,,光用maloc/free無法滿足動(dòng)態(tài)對(duì)象的要求。對(duì)象在創(chuàng)建的同時(shí)要自動(dòng)執(zhí)行構(gòu)造函數(shù),,對(duì)象在消亡之前要自動(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)對(duì)象的動(dòng)態(tài)內(nèi)存管理,見示例:
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)); // 申請(qǐng)動(dòng)態(tài)內(nèi)存
a->Initialize(); // 初始化
//…
a->Destroy(); // 清除工作
free(a); // 釋放內(nèi)存
}
void UseNewDelete(void)
{
Obj *a = new Obj; // 申請(qǐng)動(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)對(duì)象的內(nèi)存管理,應(yīng)該用new/delete,。由于內(nèi)部數(shù)據(jù)類型的“對(duì)象”沒有構(gòu)造與析構(gòu)的過程,,對(duì)它們而言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)對(duì)象”,,那么該對(duì)象因無法執(zhí)行析構(gòu)函數(shù)而可能導(dǎo)致程序出錯(cuò)。如果用delete釋放“malloc申請(qǐng)的動(dòng)態(tài)內(nèi)存”,,結(jié)果也會(huì)導(dǎo)致程序出錯(cuò),,但是該程序的可讀性很差。所以new/delete必須配對(duì)使用,,malloc/free也一樣,。
1.1.8 內(nèi)存耗盡怎么辦?
如果在申請(qǐng)動(dòng)態(tài)內(nèi)存時(shí)找不到足夠大的內(nèi)存塊,,malloc和new將返回NULL指針,,宣告內(nèi)存申請(qǐng)失敗。通常有三種方式處理“內(nèi)存耗盡”問題,。
?。?)判斷指針是否為NULL,如果是則馬上用return語句終止本函數(shù),。例如:
void Func(void)
{
A *a = new A;
if(a == NULL)
{
return;
}
…
}
?。?)判斷指針是否為NULL,如果是則馬上用exit(1)終止整個(gè)程序的運(yùn)行,。例如:
void Func(void)
{
A *a = new A;
if(a == NULL)
{
cout << “Memory Exhausted” << endl;
exit(1);
}
…
}
?。?)為new和malloc設(shè)置異常處理函數(shù)。例如Visual C++可以用_set_new_hander函數(shù)為new設(shè)置用戶自己定義的異常處理函數(shù),,也可以讓malloc享用與new相同的異常處理函數(shù),。詳細(xì)內(nèi)容請(qǐng)參考C++使用手冊(cè)。
上述(1)(2)方式使用最普遍,。如果一個(gè)函數(shù)內(nèi)有多處需要申請(qǐng)動(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)象要告訴大家。對(duì)于32位以上的應(yīng)用程序而言,,無論怎樣使用malloc與new,,幾乎不可能導(dǎo)致“內(nèi)存耗盡”。我在Windows 98下用Visual C++編寫了測試程序,,見示例7,。這個(gè)程序會(huì)無休止地運(yùn)行下去,根本不會(huì)終止,。因?yàn)?2位操作系統(tǒng)支持“虛存”,,內(nèi)存用完了,自動(dòng)用硬盤空間頂替,。我只聽到硬盤嘎吱嘎吱地響,,Window 98已經(jīng)累得對(duì)鍵盤、鼠標(biāo)毫無反應(yīng),。
我可以得出這么一個(gè)結(jié)論:對(duì)于32位以上的應(yīng)用程序,,“內(nèi)存耗盡”錯(cuò)誤處理程序毫無用處。這下可把Unix和Windows程序員們樂壞了:反正錯(cuò)誤處理程序不起作用,,我就不寫了,,省了很多麻煩。
我不想誤導(dǎo)讀者,,必須強(qiáng)調(diào):不加錯(cuò)誤處理將導(dǎo)致程序的質(zhì)量很差,,千萬不可因小失大。
void main(void)
{
float *p = NULL;
while(TRUE)
{
p = new float[1000000];
cout << “eat memory” << endl;
if(p==NULL)
exit(1);
}
}
1.1.9 malloc/free的使用要點(diǎn)
函數(shù)malloc的原型如下:
void * malloc(size_t size);
用malloc申請(qǐng)一塊長度為length的整數(shù)類型的內(nèi)存,,程序如下:
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ù)本身并不識(shí)別要申請(qǐng)的內(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é),。最好用以下程序作一次測試:
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的原型如下:
void free( void * memblock );
為什么free函數(shù)不象malloc函數(shù)那樣復(fù)雜呢?這是因?yàn)橹羔榩的類型以及它所指的內(nèi)存的容量事先都是知道的,,語句free(p)能正確地釋放內(nèi)存,。如果p是NULL指針,那么free對(duì)p無論操作多少次都不會(huì)出問題,。如果p不是NULL指針,,那么free對(duì)p連續(xù)操作兩次就會(huì)導(dǎo)致程序運(yùn)行錯(cuò)誤。
1.1.10 new/delete的使用要點(diǎn)
運(yùn)算符new使用起來要比函數(shù)malloc簡單得多,,例如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
這是因?yàn)閚ew內(nèi)置了sizeof,、類型轉(zhuǎn)換和類型安全檢查功能。對(duì)于非內(nèi)部數(shù)據(jù)類型的對(duì)象而言,,new在創(chuàng)建動(dòng)態(tài)對(duì)象的同時(shí)完成了初始化工作,。如果對(duì)象有多個(gè)構(gòu)造函數(shù),那么new的語句也可以有多種形式,。例如
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)建對(duì)象數(shù)組,,那么只能使用對(duì)象的無參數(shù)構(gòu)造函數(shù)。例如:
Obj *objects = new Obj[100]; // 創(chuàng)建100個(gè)動(dòng)態(tài)對(duì)象
不能寫成:
Obj *objects = new Obj[100](1);// 創(chuàng)建100個(gè)動(dòng)態(tài)對(duì)象的同時(shí)賦初值1
在用delete釋放對(duì)象數(shù)組時(shí),,留意不要丟了符號(hào)‘[]’,。例如:
delete []objects; // 正確的用法
delete objects; // 錯(cuò)誤的用法
后者有可能引起程序崩潰和內(nèi)存泄漏。
1.2 C++中的健壯指針和資源管理
我最喜歡的對(duì)資源的定義是:"任何在你的程序中獲得并在此后釋放的東西?quot;內(nèi)存是一個(gè)相當(dāng)明顯的資源的例子,。它需要用new來獲得,,用delete來釋放。同時(shí)也有許多其它類型的資源文件句柄,、重要的片斷,、Windows中的GDI資源,等等,。將資源的概念推廣到程序中創(chuàng)建,、釋放的所有對(duì)象也是十分方便的,無論對(duì)象是在堆中分配的還是在棧中或者是在全局作用于內(nèi)生命的,。
對(duì)于給定的資源的擁有著,,是負(fù)責(zé)釋放資源的一個(gè)對(duì)象或者是一段代碼。所有權(quán)分立為兩種級(jí)別——自動(dòng)的和顯式的(automatic and explicit),,如果一個(gè)對(duì)象的釋放是由語言本身的機(jī)制來保證的,,這個(gè)對(duì)象的就是被自動(dòng)地所有。例如,一個(gè)嵌入在其他對(duì)象中的對(duì)象,,他的清除需要其他對(duì)象來在清除的時(shí)候保證,。外面的對(duì)象被看作嵌入類的所有者。 類似地,,每個(gè)在棧上創(chuàng)建的對(duì)象(作為自動(dòng)變量)的釋放(破壞)是在控制流離開了對(duì)象被定義的作用域的時(shí)候保證的。這種情況下,,作用于被看作是對(duì)象的所有者,。注意所有的自動(dòng)所有權(quán)都是和語言的其他機(jī)制相容的,包括異常,。無論是如何退出作用域的——正常流程控制退出,、一個(gè)break語句、一個(gè)return,、一個(gè)goto,、或者是一個(gè)throw——自動(dòng)資源都可以被清除。
到目前為止,,一切都很好,!問題是在引入指針、句柄和抽象的時(shí)候產(chǎn)生的,。如果通過一個(gè)指針訪問一個(gè)對(duì)象的話,,比如對(duì)象在堆中分配,C++不自動(dòng)地關(guān)注它的釋放,。程序員必須明確的用適當(dāng)?shù)某绦蚍椒▉磲尫胚@些資源,。比如說,如果一個(gè)對(duì)象是通過調(diào)用new來創(chuàng)建的,,它需要用delete來回收,。一個(gè)文件是用CreateFile(Win32 API)打開的,它需要用CloseHandle來關(guān)閉,。用EnterCritialSection進(jìn)入的臨界區(qū)(Critical Section)需要LeaveCriticalSection退出,,等等。一個(gè)"裸"指針,,文件句柄,,或者臨界區(qū)狀態(tài)沒有所有者來確保它們的最終釋放?;镜馁Y源管理的前提就是確保每個(gè)資源都有他們的所有者,。
1.2.1 第一條規(guī)則(RAII)
一個(gè)指針,一個(gè)句柄,,一個(gè)臨界區(qū)狀態(tài)只有在我們將它們封裝入對(duì)象的時(shí)候才會(huì)擁有所有者,。這就是我們的第一規(guī)則:在構(gòu)造函數(shù)中分配資源,在析構(gòu)函數(shù)中釋放資源。
當(dāng)你按照規(guī)則將所有資源封裝的時(shí)候,,你可以保證你的程序中沒有任何的資源泄露,。這點(diǎn)在當(dāng)封裝對(duì)象(Encapsulating Object)在棧中建立或者嵌入在其他的對(duì)象中的時(shí)候非常明顯。但是對(duì)那些動(dòng)態(tài)申請(qǐng)的對(duì)象呢,?不要急,!任何動(dòng)態(tài)申請(qǐng)的東西都被看作一種資源,并且要按照上面提到的方法進(jìn)行封裝,。這一對(duì)象封裝對(duì)象的鏈不得不在某個(gè)地方終止,。它最終終止在最高級(jí)的所有者,自動(dòng)的或者是靜態(tài)的,。這些分別是對(duì)離開作用域或者程序時(shí)釋放資源的保證,。
下面是資源封裝的一個(gè)經(jīng)典例子。在一個(gè)多線程的應(yīng)用程序中,,線程之間共享對(duì)象的問題是通過用這樣一個(gè)對(duì)象聯(lián)系臨界區(qū)來解決的,。每一個(gè)需要訪問共享資源的客戶需要獲得臨界區(qū)。例如,,這可能是Win32下臨界區(qū)的實(shí)現(xiàn)方法,。
class CritSect
{
friend class Lock;
public:
CritSect () { InitializeCriticalSection (&_critSection); }
~CritSect () { DeleteCriticalSection (&_critSection); }
private:
void Acquire ()
{
EnterCriticalSection (&_critSection);
}
void Release ()
{
LeaveCriticalSection (&_critSection);
}
private:
CRITICAL_SECTION _critSection;
};
這里聰明的部分是我們確保每一個(gè)進(jìn)入臨界區(qū)的客戶最后都可以離開。"進(jìn)入"臨界區(qū)的狀態(tài)是一種資源,,并應(yīng)當(dāng)被封裝,。封裝器通常被稱作一個(gè)鎖(lock)。
class Lock
{
public:
Lock (CritSect& critSect) : _critSect (critSect)
{
_critSect.Acquire ();
}
~Lock ()
{
_critSect.Release ();
}
private
CritSect & _critSect;
};
鎖一般的用法如下:
void Shared::Act () throw (char *)
{
Lock lock (_critSect);
// perform action —— may throw
// automatic destructor of lock
}
注意無論發(fā)生什么,,臨界區(qū)都會(huì)借助于語言的機(jī)制保證釋放,。
還有一件需要記住的事情——每一種資源都需要被分別封裝。這是因?yàn)橘Y源分配是一個(gè)非常容易出錯(cuò)的操作,,是要資源是有限提供的,。我們會(huì)假設(shè)一個(gè)失敗的資源分配會(huì)導(dǎo)致一個(gè)異常——事實(shí)上,,這會(huì)經(jīng)常的發(fā)生,。所以如果你想試圖用一個(gè)石頭打兩只鳥的話,或者在一個(gè)構(gòu)造函數(shù)中申請(qǐng)兩種形式的資源,,你可能就會(huì)陷入麻煩,。只要想想在一種資源分配成功但另一種失敗拋出異常時(shí)會(huì)發(fā)生什么。因?yàn)闃?gòu)造函數(shù)還沒有全部完成,,析構(gòu)函數(shù)不可能被調(diào)用,,第一種資源就會(huì)發(fā)生泄露。
這種情況可以非常簡單的避免,。無論何時(shí)你有一個(gè)需要兩種以上資源的類時(shí),,寫兩個(gè)小的封裝器將它們嵌入你的類中,。每一個(gè)嵌入的構(gòu)造都可以保證刪除,即使包裝類沒有構(gòu)造完成,。
1.2.2 Smart Pointers
我們至今還沒有討論最常見類型的資源——用操作符new分配,,此后用指針訪問的一個(gè)對(duì)象。我們需要為每個(gè)對(duì)象分別定義一個(gè)封裝類嗎,?(事實(shí)上,,C++標(biāo)準(zhǔn)模板庫已經(jīng)有了一個(gè)模板類,叫做auto_ptr,,其作用就是提供這種封裝,。我們一會(huì)兒在回到auto_ptr。)讓我們從一個(gè)極其簡單,、呆板但安全的東西開始??聪旅娴腟mart Pointer模板類,,它十分堅(jiān)固,甚至無法實(shí)現(xiàn),。
template <class T>
class SmartPointer
{
public:
~SmartPointer () { delete _p; }
T * operator->() { return _p; }
T const * operator->() const { return _p; }
protected:
SmartPointer (): _p (0) {}
explicit SmartPointer (T* p): _p (p) {}
T * _p;
};
為什么要把SmartPointer的構(gòu)造函數(shù)設(shè)計(jì)為protected呢,?如果我需要遵守第一條規(guī)則,那么我就必須這樣做,。資源——在這里是class T的一個(gè)對(duì)象——必須在封裝器的構(gòu)造函數(shù)中分配,。但是我不能只簡單的調(diào)用new T,因?yàn)槲也恢繲的構(gòu)造函數(shù)的參數(shù),。因?yàn)?,在原則上,每一個(gè)T都有一個(gè)不同的構(gòu)造函數(shù),;我需要為他定義個(gè)另外一個(gè)封裝器,。模板的用處會(huì)很大,為每一個(gè)新的類,,我可以通過繼承SmartPointer定義一個(gè)新的封裝器,,并且提供一個(gè)特定的構(gòu)造函數(shù)。
class SmartItem: public SmartPointer<Item>
{
public:
explicit SmartItem (int i)
: SmartPointer<Item> (new Item (i)) {}
};
為每一個(gè)類提供一個(gè)Smart Pointer真的值得嗎,?說實(shí)話——不,!他很有教學(xué)的價(jià)值,但是一旦你學(xué)會(huì)如何遵循第一規(guī)則的話,,你就可以放松規(guī)則并使用一些高級(jí)的技術(shù),。這一技術(shù)是讓SmartPointer的構(gòu)造函數(shù)成為public,但是只是是用它來做資源轉(zhuǎn)換(Resource Transfer)我的意思是用new操作符的結(jié)果直接作為SmartPointer的構(gòu)造函數(shù)的參數(shù),,像這樣:
SmartPointer<Item> item (new Item (i));
這個(gè)方法明顯更需要自控性,,不只是你,,而且包括你的程序小組的每個(gè)成員。他們都必須發(fā)誓出了作資源轉(zhuǎn)換外不把構(gòu)造函數(shù)用在人以其他用途,。幸運(yùn)的是,,這條規(guī)矩很容易得以加強(qiáng)。只需要在源文件中查找所有的new即可,。
1.2.3 Resource Transfer
到目前為止,,我們所討論的一直是生命周期在一個(gè)單獨(dú)的作用域內(nèi)的資源。現(xiàn)在我們要解決一個(gè)困難的問題——如何在不同的作用域間安全的傳遞資源,。這一問題在當(dāng)你處理容器的時(shí)候會(huì)變得十分明顯,。你可以動(dòng)態(tài)的創(chuàng)建一串對(duì)象,將它們存放至一個(gè)容器中,,然后將它們?nèi)〕?,并且在最終安排它們。為了能夠讓這安全的工作——沒有泄露——對(duì)象需要改變其所有者,。
這個(gè)問題的一個(gè)非常顯而易見的解決方法是使用Smart Pointer,,無論是在加入容器前還是還找到它們以后。這是他如何運(yùn)作的,,你加入Release方法到Smart Pointer中:
template <class T>
T * SmartPointer<T>::Release ()
{
T * pTmp = _p;
_p = 0;
return pTmp;
}
注意在Release調(diào)用以后,,Smart Pointer就不再是對(duì)象的所有者了——它內(nèi)部的指針指向空。現(xiàn)在,,調(diào)用了Release都必須是一個(gè)負(fù)責(zé)的人并且迅速隱藏返回的指針到新的所有者對(duì)象中,。在我們的例子中,容器調(diào)用了Release,,比如這個(gè)Stack的例子:
void Stack::Push (SmartPointer <Item> & item) throw (char *)
{
if (_top == maxStack)
throw "Stack overflow";
_arr [_top++] = item.Release ();
};
同樣的,,你也可以再你的代碼中用加強(qiáng)Release的可靠性。
相應(yīng)的Pop方法要做些什么呢,?他應(yīng)該釋放了資源并祈禱調(diào)用它的是一個(gè)負(fù)責(zé)的人而且立即作一個(gè)資源傳遞它到一個(gè)Smart Pointer,?這聽起來并不好。
1.2.4 Strong Pointers
資源管理在內(nèi)容索引(Windows NT Server上的一部分,,現(xiàn)在是Windows 2000)上工作,,并且,我對(duì)這十分滿意,。然后我開始想……這一方法是在這樣一個(gè)完整的系統(tǒng)中形成的,,如果可以把它內(nèi)建入語言的本身豈不是一件非常好?我提出了強(qiáng)指針(Strong Pointer)和弱指針(Weak Pointer),。一個(gè)Strong Pointer會(huì)在許多地方和我們這個(gè)SmartPointer相似--它在超出它的作用域后會(huì)清除他所指向的對(duì)象,。資源傳遞會(huì)以強(qiáng)指針賦值的形式進(jìn)行。也可以有Weak Pointer存在,,它們用來訪問對(duì)象而不需要所有對(duì)象--比如可賦值的引用,。
任何指針都必須聲明為Strong或者Weak,,并且語言應(yīng)該來關(guān)注類型轉(zhuǎn)換的規(guī)定。例如,,你不可以將Weak Pointer傳遞到一個(gè)需要Strong Pointer的地方,,但是相反卻可以。Push方法可以接受一個(gè)Strong Pointer并且將它轉(zhuǎn)移到Stack中的Strong Pointer的序列中,。Pop方法將會(huì)返回一個(gè)Strong Pointer,。把Strong Pointer的引入語言將會(huì)使垃圾回收成為歷史。
這里還有一個(gè)小問題--修改C++標(biāo)準(zhǔn)幾乎和競選美國總統(tǒng)一樣容易,。當(dāng)我將我的注意告訴給Bjarne Stroutrup的時(shí)候,,他看我的眼神好像是我剛剛要向他借一千美元一樣。
然后我突然想到一個(gè)念頭,。我可以自己實(shí)現(xiàn)Strong Pointers,。畢竟,它們都很想Smart Pointers,。給它們一個(gè)拷貝構(gòu)造函數(shù)并重載賦值操作符并不是一個(gè)大問題,。事實(shí)上,這正是標(biāo)準(zhǔn)庫中的auto_ptr有的,。重要的是對(duì)這些操作給出一個(gè)資源轉(zhuǎn)移的語法,,但是這也不是很難,。
template <class T>
SmartPointer<T>::SmartPointer (SmartPointer<T> & ptr)
{
_p = ptr.Release ();
}
template <class T>
void SmartPointer<T>::operator = (SmartPointer<T> & ptr)
{
if (_p != ptr._p)
{
delete _p;
_p = ptr.Release ();
}
}
使這整個(gè)想法迅速成功的原因之一是我可以以值方式傳遞這種封裝指針,!我有了我的蛋糕,并且也可以吃了,??催@個(gè)Stack的新的實(shí)現(xiàn):
class Stack
{
enum { maxStack = 3 };
public:
Stack ()
: _top (0)
{}
void Push (SmartPointer<Item> & item) throw (char *)
{
if (_top >= maxStack)
throw "Stack overflow";
_arr [_top++] = item;
}
SmartPointer<Item> Pop ()
{
if (_top == 0)
return SmartPointer<Item> ();
return _arr [--_top];
}
private
int _top;
SmartPointer<Item> _arr [maxStack];
};
Pop方法強(qiáng)制客戶將其返回值賦給一個(gè)Strong Pointer,SmartPointer<Item>。任何試圖將他對(duì)一個(gè)普通指針的賦值都會(huì)產(chǎn)生一個(gè)編譯期錯(cuò)誤,,因?yàn)轭愋筒黄ヅ?。此外,因?yàn)镻op以值方式返回一個(gè)Strong Pointer(在Pop的聲明時(shí)SmartPointer<Item>后面沒有&符號(hào)),,編譯器在return時(shí)自動(dòng)進(jìn)行了一個(gè)資源轉(zhuǎn)換,。他調(diào)用了operator =來從數(shù)組中提取一個(gè)Item,拷貝構(gòu)造函數(shù)將他傳遞給調(diào)用者。調(diào)用者最后擁有了指向Pop賦值的Strong Pointer指向的一個(gè)Item,。
我馬上意識(shí)到我已經(jīng)在某些東西之上了,。我開始用了新的方法重寫原來的代碼。
1.2.5 Parser
我過去有一個(gè)老的算術(shù)操作分析器,,是用老的資源管理的技術(shù)寫的,。分析器的作用是在分析樹中生成節(jié)點(diǎn),節(jié)點(diǎn)是動(dòng)態(tài)分配的,。例如分析器的Expression方法生成一個(gè)表達(dá)式節(jié)點(diǎn),。我沒有時(shí)間用Strong Pointer去重寫這個(gè)分析器,。我令Expression、Term和Factor方法以傳值的方式將Strong Pointer返回到Node中,??聪旅娴腅xpression方法的實(shí)現(xiàn):
SmartPointer<Node> Parser::Expression()
{
// Parse a term
SmartPointer<Node> pNode = Term ();
EToken token = _scanner.Token();
if ( token == tPlus || token == tMinus )
{
// Expr := Term { (‘+‘ | ‘-‘) Term }
SmartPointer<MultiNode> pMultiNode = new SumNode (pNode);
do
{
_scanner.Accept();
SmartPointer<Node> pRight = Term ();
pMultiNode->AddChild (pRight, (token == tPlus));
token = _scanner.Token();
} while (token == tPlus || token == tMinus);
pNode = up_cast<Node, MultiNode> (pMultiNode);
}
// otherwise Expr := Term
return pNode; // by value!
}
最開始,Term方法被調(diào)用,。他傳值返回一個(gè)指向Node的Strong Pointer并且立刻把它保存到我們自己的Strong Pointer,pNode中,。如果下一個(gè)符號(hào)不是加號(hào)或者減號(hào),我們就簡單的把這個(gè)SmartPointer以值返回,,這樣就釋放了Node的所有權(quán),。另外一方面,如果下一個(gè)符號(hào)是加號(hào)或者減號(hào),,我們創(chuàng)建一個(gè)新的SumMode并且立刻(直接傳遞)將它儲(chǔ)存到MultiNode的一個(gè)Strong Pointer中,。這里,SumNode是從MultiMode中繼承而來的,,而MulitNode是從Node繼承而來的,。原來的Node的所有權(quán)轉(zhuǎn)給了SumNode。
只要是他們?cè)诒患犹?hào)和減號(hào)分開的時(shí)候,,我們就不斷的創(chuàng)建terms,,我們將這些term轉(zhuǎn)移到我們的MultiNode中,同時(shí)MultiNode得到了所有權(quán),。最后,,我們將指向MultiNode的Strong Pointer向上映射為指向Mode的Strong Pointer,并且將他返回調(diào)用著,。
我們需要對(duì)Strong Pointers進(jìn)行顯式的向上映射,,即使指針是被隱式的封裝。例如,,一個(gè)MultiNode是一個(gè)Node,,但是相同的is-a關(guān)系在SmartPointer<MultiNode>和SmartPointer<Node>之間并不存在,因?yàn)樗鼈兪欠蛛x的類(模板實(shí)例)并不存在繼承關(guān)系,。up-cast模板是像下面這樣定義的:
template<class To, class From>
inline SmartPointer<To> up_cast (SmartPointer<From> & from)
{
return SmartPointer<To> (from.Release ());
}
如果你的編譯器支持新加入標(biāo)準(zhǔn)的成員模板(member template)的話,,你可以為SmartPointer<T>定義一個(gè)新的構(gòu)造函數(shù)用來從接受一個(gè)class U。
template <class T>
template <class U> SmartPointer<T>::SmartPointer (SPrt<U> & uptr)
: _p (uptr.Release ())
{}
這里的這個(gè)花招是模板在U不是T的子類的時(shí)候就不會(huì)編譯成功(換句話說,,只在U is-a T的時(shí)候才會(huì)編譯),。這是因?yàn)閡ptr的緣故。Release()方法返回一個(gè)指向U的指針,,并被賦值為_p,,一個(gè)指向T的指針。所以如果U不是一個(gè)T的話,,賦值會(huì)導(dǎo)致一個(gè)編譯時(shí)刻錯(cuò)誤,。
std::auto_ptr
后來我意識(shí)到在STL中的auto_ptr模板,,就是我的Strong Pointer,。在那時(shí)候還有許多的實(shí)現(xiàn)差異(auto_ptr的Release方法并不將內(nèi)部的指針清零--你的編譯器的庫很可能用的就是這種陳舊的實(shí)現(xiàn)),但是最后在標(biāo)準(zhǔn)被廣泛接受之前都被解決了。
1.2.6 Transfer Semantics
目前為止,,我們一直在討論在C++程序中資源管理的方法,。宗旨是將資源封裝到一些輕量級(jí)的類中,,并由類負(fù)責(zé)它們的釋放,。特別的是,所有用new操作符分配的資源都會(huì)被儲(chǔ)存并傳遞進(jìn)Strong Pointer(標(biāo)準(zhǔn)庫中的auto_ptr)的內(nèi)部,。
這里的關(guān)鍵詞是傳遞(passing),。一個(gè)容器可以通過傳值返回一個(gè)Strong Pointer來安全的釋放資源。容器的客戶只能夠通過提供一個(gè)相應(yīng)的Strong Pointer來保存這個(gè)資源,。任何一個(gè)將結(jié)果賦給一個(gè)"裸"指針的做法都立即會(huì)被編譯器發(fā)現(xiàn),。
auto_ptr<Item> item = stack.Pop (); // ok
Item * p = stack.Pop (); // Error! Type mismatch.
以傳值方式被傳遞的對(duì)象有value semantics 或者稱為 copy semantics。Strong Pointers是以值方式傳遞的--但是我們能說它們有copy semantics嗎,?不是這樣的,!它們所指向的對(duì)象肯定沒有被拷貝過。事實(shí)上,,傳遞過后,,源auto_ptr不在訪問原有的對(duì)象,并且目標(biāo)auto_ptr成為了對(duì)象的唯一擁有者(但是往往auto_ptr的舊的實(shí)現(xiàn)即使在釋放后仍然保持著對(duì)對(duì)象的所有權(quán)),。自然而然的我們可以將這種新的行為稱作Transfer Semantics,。
拷貝構(gòu)造函數(shù)(copy construcor)和賦值操作符定義了auto_ptr的Transfer Semantics,它們用了非const的auto_ptr引用作為它們的參數(shù),。
auto_ptr (auto_ptr<T> & ptr);
auto_ptr & operator = (auto_ptr<T> & ptr);
這是因?yàn)樗鼈兇_實(shí)改變了他們的源--剝奪了對(duì)資源的所有權(quán),。
通過定義相應(yīng)的拷貝構(gòu)造函數(shù)和重載賦值操作符,,你可以將Transfer Semantics加入到許多對(duì)象中,。例如,許多Windows中的資源,,比如動(dòng)態(tài)建立的菜單或者位圖,,可以用有Transfer Semantics的類來封裝。
1.2.7 Strong Vectors
標(biāo)準(zhǔn)庫只在auto_ptr中支持資源管理,。甚至連最簡單的容器也不支持ownership semantics,。你可能想將auto_ptr和標(biāo)準(zhǔn)容器組合到一起可能會(huì)管用,但是并不是這樣的,。例如,,你可能會(huì)這樣做,但是會(huì)發(fā)現(xiàn)你不能夠用標(biāo)準(zhǔn)的方法來進(jìn)行索引,。
vector< auto_ptr<Item> > autoVector;
這種建造不會(huì)編譯成功,;
Item * item = autoVector [0];
另一方面,,這會(huì)導(dǎo)致一個(gè)從autoVect到auto_ptr的所有權(quán)轉(zhuǎn)換:
auto_ptr<Item> item = autoVector [0];
我們沒有選擇,只能夠構(gòu)造我們自己的Strong Vector,。最小的接口應(yīng)該如下:
template <class T>
class auto_vector
{
public:
explicit auto_vector (size_t capacity = 0);
T const * operator [] (size_t i) const;
T * operator [] (size_t i);
void assign (size_t i, auto_ptr<T> & p);
void assign_direct (size_t i, T * p);
void push_back (auto_ptr<T> & p);
auto_ptr<T> pop_back ();
};
你也許會(huì)發(fā)現(xiàn)一個(gè)非常防御性的設(shè)計(jì)態(tài)度,。我決定不提供一個(gè)對(duì)vector的左值索引的訪問,取而代之,,如果你想設(shè)定(set)一個(gè)值的話,,你必須用assign或者assign_direct方法。我的觀點(diǎn)是,,資源管理不應(yīng)該被忽視,,同時(shí),也不應(yīng)該在所有的地方濫用,。在我的經(jīng)驗(yàn)里,,一個(gè)strong vector經(jīng)常被許多push_back方法充斥著。
Strong vector最好用一個(gè)動(dòng)態(tài)的Strong Pointers的數(shù)組來實(shí)現(xiàn):
template <class T>
class auto_vector
{
private
void grow (size_t reqCapacity);
auto_ptr<T> *_arr;
size_t _capacity;
size_t _end;
};
grow方法申請(qǐng)了一個(gè)很大的auto_ptr<T>的數(shù)組,,將所有的東西從老的書組類轉(zhuǎn)移出來,,在其中交換,并且刪除原來的數(shù)組,。
auto_vector的其他實(shí)現(xiàn)都是十分直接的,,因?yàn)樗匈Y源管理的復(fù)雜度都在auto_ptr中。例如,,assign方法簡單的利用了重載的賦值操作符來刪除原有的對(duì)象并轉(zhuǎn)移資源到新的對(duì)象:
void assign (size_t i, auto_ptr<T> & p)
{
_arr [i] = p;
}
我已經(jīng)討論了push_back和pop_back方法,。push_back方法傳值返回一個(gè)auto_ptr,因?yàn)樗鼘⑺袡?quán)從auto_vector轉(zhuǎn)換到auto_ptr中,。
對(duì)auto_vector的索引訪問是借助auto_ptr的get方法來實(shí)現(xiàn)的,,get簡單的返回一個(gè)內(nèi)部指針。
T * operator [] (size_t i)
{
return _arr [i].get ();
}
沒有容器可以沒有iterator,。我們需要一個(gè)iterator讓auto_vector看起來更像一個(gè)普通的指針向量,。特別是,當(dāng)我們廢棄iterator的時(shí)候,,我們需要的是一個(gè)指針而不是auto_ptr,。我們不希望一個(gè)auto_vector的iterator在無意中進(jìn)行資源轉(zhuǎn)換。
template<class T>
class auto_iterator: public
iterator<random_access_iterator_tag, T *>
{
public:
auto_iterator () : _pp (0) {}
auto_iterator (auto_ptr<T> * pp) : _pp (pp) {}
bool operator != (auto_iterator<T> const & it) const
{ return it._pp != _pp; }
auto_iterator const & operator++ (int) { return _pp++; }
auto_iterator operator++ () { return ++_pp; }
T * operator * () { return _pp->get (); }
private
auto_ptr<T> * _pp;
};
我們給auto_vect提供了標(biāo)準(zhǔn)的begin和end方法來找回iterator:
class auto_vector
{
public:
typedef auto_iterator<T> iterator;
iterator begin () { return _arr; }
iterator end () { return _arr + _end; }
};
你也許會(huì)問我們是否要利用資源管理重新實(shí)現(xiàn)每一個(gè)標(biāo)準(zhǔn)的容器,?幸運(yùn)的是,,不;事實(shí)是strong vector解決了大部分所有權(quán)的需求。當(dāng)你把你的對(duì)象都安全的放置到一個(gè)strong vector中,,你可以用所有其它的容器來重新安排(weak)pointer,。
設(shè)想,例如,你需要對(duì)一些動(dòng)態(tài)分配的對(duì)象排序的時(shí)候,。你將它們的指針保存到一個(gè)strong vector中,。然后你用一個(gè)標(biāo)準(zhǔn)的vector來保存從strong vector中獲得的weak指針。你可以用標(biāo)準(zhǔn)的算法對(duì)這個(gè)vector進(jìn)行排序,。這種中介vector叫做permutation vector,。相似的,你也可以用標(biāo)準(zhǔn)的maps, priority queues, heaps, hash tables等等,。
1.2.8 Code Inspection
如果你嚴(yán)格遵照資源管理的條款,,你就不會(huì)再資源泄露或者兩次刪除的地方遇到麻煩。你也降低了訪問野指針的幾率,。同樣的,,遵循原有的規(guī)則,用delete刪除用new申請(qǐng)的德指針,,不要兩次刪除一個(gè)指針,。你也不會(huì)遇到麻煩。但是,,那個(gè)是更好的注意呢,?
這兩個(gè)方法有一個(gè)很大的不同點(diǎn)。就是和尋找傳統(tǒng)方法的bug相比,,找到違反資源管理的規(guī)定要容易的多,。后者僅需要一個(gè)代碼檢測或者一個(gè)運(yùn)行測試,而前者則在代碼中隱藏得很深,,并需要很深的檢查,。
設(shè)想你要做一段傳統(tǒng)的代碼的內(nèi)存泄露檢查。第一件事,,你要做的就是grep所有在代碼中出現(xiàn)的new,,你需要找出被分配空間地指針都作了什么。你需要確定導(dǎo)致刪除這個(gè)指針的所有的執(zhí)行路徑,。你需要檢查break語句,,過程返回,異常,。原有的指針可能賦給另一個(gè)指針,,你對(duì)這個(gè)指針也要做相同的事,。
相比之下,,對(duì)于一段用資源管理技術(shù)實(shí)現(xiàn)的代碼。你也用grep檢查所有的new,,但是這次你只需要檢查鄰近的調(diào)用:
● 這是一個(gè)直接的Strong Pointer轉(zhuǎn)換,,還是我們?cè)谝粋€(gè)構(gòu)造函數(shù)的函數(shù)體中?
● 調(diào)用的返回知是否立即保存到對(duì)象中,,構(gòu)造函數(shù)中是否有可以產(chǎn)生異常的代碼,。,?
● 如果這樣的話析構(gòu)函數(shù)中時(shí)候有delete?
下一步,你需要用grep查找所有的release方法,,并實(shí)施相同的檢查,。
不同點(diǎn)是需要檢查、理解單個(gè)執(zhí)行路徑和只需要做一些本地的檢驗(yàn),。這難道不是提醒你非結(jié)構(gòu)化的和結(jié)構(gòu)化的程序設(shè)計(jì)的不同嗎,?原理上,你可以認(rèn)為你可以應(yīng)付goto,,并且跟蹤所有的可能分支,。另一方面,你可以將你的懷疑本地化為一段代碼,。本地化在兩種情況下都是關(guān)鍵所在,。
在資源管理中的錯(cuò)誤模式也比較容易調(diào)試。最常見的bug是試圖訪問一個(gè)釋放過的strong pointer,。這將導(dǎo)致一個(gè)錯(cuò)誤,,并且很容易跟蹤。
1.2.9 共享的所有權(quán)
為每一個(gè)程序中的資源都找出或者指定一個(gè)所有者是一件很容易的事情嗎,?答案是出乎意料的,,是!如果你發(fā)現(xiàn)了一些問題,,這可能說明你的設(shè)計(jì)上存在問題,。還有另一種情況就是共享所有權(quán)是最好的甚至是唯一的選擇。
共享的責(zé)任分配給被共享的對(duì)象和它的客戶(client),。一個(gè)共享資源必須為它的所有者保持一個(gè)引用計(jì)數(shù),。另一方面,所有者再釋放資源的時(shí)候必須通報(bào)共享對(duì)象,。最后一個(gè)釋放資源的需要在最后負(fù)責(zé)free的工作,。
最簡單的共享的實(shí)現(xiàn)是共享對(duì)象繼承引用計(jì)數(shù)的類RefCounted:
class RefCounted
{
public:
RefCounted () : _count (1) {}
int GetRefCount () const { return _count; }
void IncRefCount () { _count++; }
int DecRefCount () { return --_count; }
private
int _count;
};
按照資源管理,一個(gè)引用計(jì)數(shù)是一種資源,。如果你遵守它,,你需要釋放它。當(dāng)你意識(shí)到這一事實(shí)的時(shí)候,,剩下的就變得簡單了,。簡單的遵循規(guī)則--再構(gòu)造函數(shù)中獲得引用計(jì)數(shù),在析構(gòu)函數(shù)中釋放,。甚至有一個(gè)RefCounted的smart pointer等價(jià)物:
template <class T>
class RefPtr
{
public:
RefPtr (T * p) : _p (p) {}
RefPtr (RefPtr<T> & p)
{
_p = p._p;
_p->IncRefCount ();
}
~RefPtr ()
{
if (_p->DecRefCount () == 0)
delete _p;
}
private
T * _p;
};
注意模板中的T不比成為RefCounted的后代,,但是它必須有IncRefCount和DecRefCount的方法。當(dāng)然,一個(gè)便于使用的RefPtr需要有一個(gè)重載的指針訪問操作符,。在RefPtr中加入轉(zhuǎn)換語義學(xué)(transfer semantics)是讀者的工作,。
1.2.10 所有權(quán)網(wǎng)絡(luò)
鏈表是資源管理分析中的一個(gè)很有意思的例子。如果你選擇表成為鏈(link)的所有者的話,,你會(huì)陷入實(shí)現(xiàn)遞歸的所有權(quán),。每一個(gè)link都是它的繼承者的所有者,并且,,相應(yīng)的,,余下的鏈表的所有者。下面是用smart pointer實(shí)現(xiàn)的一個(gè)表單元:
class Link
{
// ...
private
auto_ptr<Link> _next;
};
最好的方法是,,將連接控制封裝到一個(gè)弄構(gòu)進(jìn)行資源轉(zhuǎn)換的類中,。
對(duì)于雙鏈表呢?安全的做法是指明一個(gè)方向,,如forward:
class DoubleLink
{
// ...
private
DoubleLink *_prev;
auto_ptr<DoubleLink> _next;
};
注意不要?jiǎng)?chuàng)建環(huán)形鏈表,。
這給我們帶來了另外一個(gè)有趣的問題--資源管理可以處理環(huán)形的所有權(quán)嗎?它可以,,用一個(gè)mark-and-sweep的算法,。這里是實(shí)現(xiàn)這種方法的一個(gè)例子:
template<class T>
class CyclPtr
{
public:
CyclPtr (T * p)
:_p (p), _isBeingDeleted (false)
{}
~CyclPtr ()
{
_isBeingDeleted = true;
if (!_p->IsBeingDeleted ())
delete _p;
}
void Set (T * p)
{
_p = p;
}
bool IsBeingDeleted () const { return _isBeingDeleted; }
private
T * _p;
bool _isBeingDeleted;
};
注意我們需要用class T來實(shí)現(xiàn)方法IsBeingDeleted,就像從CyclPtr繼承,。對(duì)特殊的所有權(quán)網(wǎng)絡(luò)普通化是十分直接的,。
將原有代碼轉(zhuǎn)換為資源管理代碼
如果你是一個(gè)經(jīng)驗(yàn)豐富的程序員,你一定會(huì)知道找資源的bug是一件浪費(fèi)時(shí)間的痛苦的經(jīng)歷,。我不必說服你和你的團(tuán)隊(duì)花費(fèi)一點(diǎn)時(shí)間來熟悉資源管理是十分值得的,。你可以立即開始用這個(gè)方法,無論你是在開始一個(gè)新項(xiàng)目或者是在一個(gè)項(xiàng)目的中期,。轉(zhuǎn)換不必立即全部完成,。下面是步驟。
(1) 首先,,(2) 在你的工程中建立基本的Strong Pointer,。然后通過查找代碼中的new來開始封裝裸指(3) 針。
(4) 最先封裝的是在過程中定義的臨時(shí)指(5) 針,。簡單的將它們替換為auto_ptr并且刪除相應(yīng)的delete,。如果一個(gè)指(6) 針在過程中沒有被刪除而(7) 是被返回,(8) 用auto_ptr替換并在返回前調(diào)用release方法,。在你做第二次傳遞的時(shí)候,,(9) 你需要處理對(duì)release的調(diào)用。注意,,(10) 即使是在這點(diǎn),,(11) 你的代碼也可能更加"精力充沛"--你會(huì)移出代碼中潛在的資源泄漏問題,。
(12) 下面是指(13) 向資源的裸指(14) 針,。確保它們被獨(dú)立的封裝到auto_ptr中,,(15) 或者在構(gòu)造函數(shù)中分配在析構(gòu)函數(shù)中釋放。如果你有傳遞所有權(quán)的行為的話,,(16) 需要調(diào)用release方法,。如果你有容器所有對(duì)象,(17) 用Strong Pointers重新實(shí)現(xiàn)它們,。
(18) 接下來,,(19) 找到所有對(duì)release的方法調(diào)用并且盡力清除所有,(20) 如果一個(gè)release調(diào)用返回一個(gè)指(21) 針,,(22) 將它修改傳值返回一個(gè)auto_ptr,。
(23) 重復(fù)(24) 著一過程,(25) 直到最后所有new和release的調(diào)用都在構(gòu)造函數(shù)或者資源轉(zhuǎn)換的時(shí)候發(fā)生,。這樣,,(26) 你在你的代碼中處理了資源泄漏的問題。對(duì)其他資源進(jìn)行相似的操作,。
(27) 你會(huì)發(fā)現(xiàn)資源管理清除了許多錯(cuò)誤和異常處理帶來的復(fù)(28) 雜性,。不(29) 僅僅你的代碼會(huì)變得精力充沛,(30) 它也會(huì)變得簡單并容易維護(hù),。
2 內(nèi)存泄漏
2.1 C++中動(dòng)態(tài)內(nèi)存分配引發(fā)問題的解決方案
假設(shè)我們要開發(fā)一個(gè)String類,,它可以方便地處理字符串?dāng)?shù)據(jù)。我們可以在類中聲明一個(gè)數(shù)組,,考慮到有時(shí)候字符串極長,,我們可以把數(shù)組大小設(shè)為200,但一般的情況下又不需要這么多的空間,,這樣是浪費(fèi)了內(nèi)存,。對(duì)了,我們可以使用new操作符,,這樣是十分靈活的,,但在類中就會(huì)出現(xiàn)許多意想不到的問題,本文就是針對(duì)這一現(xiàn)象而寫的?,F(xiàn)在,,我們先來開發(fā)一個(gè)String類,但它是一個(gè)不完善的類,。的確,,我們要刻意地使它出現(xiàn)各種各樣的問題,這樣才好對(duì)癥下藥,。好了,,我們開始吧,!
/* String.h */
#ifndef STRING_H_
#define STRING_H_
class String
{
private:
char * str; //存儲(chǔ)數(shù)據(jù)
int len; //字符串長度
public:
String(const char * s); //構(gòu)造函數(shù)
String(); // 默認(rèn)構(gòu)造函數(shù)
~String(); // 析構(gòu)函數(shù)
friend ostream & operator<<(ostream & os,const String& st);
};
#endif
/*String.cpp*/
#include <iostream>
#include <cstring>
#include "String.h"
using namespace std;
String::String(const char * s)
{
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
}//拷貝數(shù)據(jù)
String::String()
{
len =0;
str = new char[len+1];
str[0]=‘{post.abstract}‘;
}
String::~String()
{
cout<<"這個(gè)字符串將被刪除:"<<str<<‘\n‘;//為了方便觀察結(jié)果,特留此行代碼,。
delete [] str;
}
ostream & operator<<(ostream & os, const String & st)
{
os << st.str;
return os;
}
/*test_right.cpp*/
#include <iostream>
#include <stdlib.h>
#include "String.h"
using namespace std;
int main()
{
String temp("天極網(wǎng)");
cout<<temp<<‘\n‘;
system("PAUSE");
return 0;
}
運(yùn)行結(jié)果:
天極網(wǎng)
請(qǐng)按任意鍵繼續(xù). . .
大家可以看到,,以上程序十分正確,而且也是十分有用的,??墒牵覀儾荒鼙槐砻娆F(xiàn)象所迷惑,!下面,,請(qǐng)大家用test_String.cpp文件替換test_right.cpp文件進(jìn)行編譯,看看結(jié)果,。有的編譯器可能就是根本不能進(jìn)行編譯,!
test_String.cpp:
#include <iostream>
#include <stdlib.h>
#include "String.h"
using namespace std;
void show_right(const String&);
void show_String(const String);//注意,參數(shù)非引用,,而是按值傳遞,。
int main()
{
String test1("第一個(gè)范例。");
String test2("第二個(gè)范例,。");
String test3("第三個(gè)范例,。");
String test4("第四個(gè)范例。");
cout<<"下面分別輸入三個(gè)范例:\n";
cout<<test1<<endl;
cout<<test2<<endl;
cout<<test3<<endl;
String* String1=new String(test1);
cout<<*String1<<endl;
delete String1;
cout<<test1<<endl; //在Dev-cpp上沒有任何反應(yīng),。
cout<<"使用正確的函數(shù):"<<endl;
show_right(test2);
cout<<test2<<endl;
cout<<"使用錯(cuò)誤的函數(shù):"<<endl;
show_String(test2);
cout<<test2<<endl; //這一段代碼出現(xiàn)嚴(yán)重的錯(cuò)誤,!
String String2(test3);
cout<<"String2: "<<String2<<endl;
String String3;
String3=test4;
cout<<"String3: "<<String3<<endl;
cout<<"下面,程序結(jié)束,,析構(gòu)函數(shù)將被調(diào)用,。"<<endl;
return 0;
}
void show_right(const String& a)
{
cout<<a<<endl;
}
void show_String(const String a)
{
cout<<a<<endl;
}
運(yùn)行結(jié)果:
下面分別輸入三個(gè)范例:
第一個(gè)范例。
第二個(gè)范例,。
第三個(gè)范例,。
第一個(gè)范例。
這個(gè)字符串將被刪除:第一個(gè)范例,。
使用正確的函數(shù):
第二個(gè)范例,。
第二個(gè)范例。
使用錯(cuò)誤的函數(shù):
第二個(gè)范例,。
這個(gè)字符串將被刪除:第二個(gè)范例,。
這個(gè)字符串將被刪除:?=
=
String2: 第三個(gè)范例。
String3: 第四個(gè)范例,。
下面,,程序結(jié)束,析構(gòu)函數(shù)將被調(diào)用,。
這個(gè)字符串將被刪除:第四個(gè)范例,。
這個(gè)字符串將被刪除:第三個(gè)范例,。
這個(gè)字符串將被刪除:?=
這個(gè)字符串將被刪除:x =
這個(gè)字符串將被刪除:?=
這個(gè)字符串將被刪除:
現(xiàn)在,請(qǐng)大家自己試試運(yùn)行結(jié)果,,或許會(huì)更加慘不忍睹呢,!下面,我為大家一一分析原因,。
首先,,大家要知道,,C++類有以下這些極為重要的函數(shù):
一:復(fù)制構(gòu)造函數(shù),。
二:賦值函數(shù)。
我們先來講復(fù)制構(gòu)造函數(shù),。什么是復(fù)制構(gòu)造函數(shù)呢,?比如,我們可以寫下這樣的代碼:String test1(test2);這是進(jìn)行初始化,。我們知道,,初始化對(duì)象要用構(gòu)造函數(shù)??蛇@兒呢,?按理說,應(yīng)該有聲明為這樣的構(gòu)造函數(shù):String(const String &);可是,,我們并沒有定義這個(gè)構(gòu)造函數(shù)呀,?答案是,C++提供了默認(rèn)的復(fù)制構(gòu)造函數(shù),,問題也就出在這兒,。
(1):什么時(shí)候會(huì)調(diào)用復(fù)制構(gòu)造函數(shù)呢?(以String類為例,。)
在我們提供這樣的代碼:String test1(test2)時(shí),,它會(huì)被調(diào)用;當(dāng)函數(shù)的參數(shù)列表為按值傳遞,,也就是沒有用引用和指針作為類型時(shí),,如:void show_String(const String),它會(huì)被調(diào)用,。其實(shí),,還有一些情況,但在這兒就不列舉了,。
(2):它是什么樣的函數(shù),。
它的作用就是把兩個(gè)類進(jìn)行復(fù)制。拿String類為例,,C++提供的默認(rèn)復(fù)制構(gòu)造函數(shù)是這樣的:
String(const String& a)
{
str=a.str;
len=a.len;
}
在平時(shí),,這樣并不會(huì)有任何的問題出現(xiàn),,但我們用了new操作符,涉及到了動(dòng)態(tài)內(nèi)存分配,,我們就不得不談?wù)劀\復(fù)制和深復(fù)制了,。以上的函數(shù)就是實(shí)行的淺復(fù)制,它只是復(fù)制了指針,,而并沒有復(fù)制指針指向的數(shù)據(jù),,可謂一點(diǎn)兒用也沒有。打個(gè)比方吧,!就像一個(gè)朋友讓你把一個(gè)程序通過網(wǎng)絡(luò)發(fā)給他,,而你大大咧咧地把快捷方式發(fā)給了他,有什么用處呢,?我們來具體談?wù)劊?/p>
假如,,A對(duì)象中存儲(chǔ)了這樣的字符串:“C++”。它的地址為2000?,F(xiàn)在,,我們把A對(duì)象賦給B對(duì)象:String B=A。現(xiàn)在,,A和B對(duì)象的str指針均指向2000地址,。看似可以使用,,但如果B對(duì)象的析構(gòu)函數(shù)被調(diào)用時(shí),,則地址2000處的字符串“C++”已經(jīng)被從內(nèi)存中抹去,而A對(duì)象仍然指向地址2000,。這時(shí),,如果我們寫下這樣的代碼:cout<<A<<endl;或是等待程序結(jié)束,A對(duì)象的析構(gòu)函數(shù)被調(diào)用時(shí),,A對(duì)象的數(shù)據(jù)能否顯示出來呢,?只會(huì)是亂碼。而且,,程序還會(huì)這樣做:連續(xù)對(duì)地址2000處使用兩次delete操作符,,這樣的后果是十分嚴(yán)重的!
本例中,,有這樣的代碼:
String* String1=new String(test1);
cout<<*String1<<endl;
delete String1;
假設(shè)test1中str指向的地址為2000,而String中str指針同樣指向地址2000,,我們刪除了2000處的數(shù)據(jù),而test1對(duì)象呢,?已經(jīng)被破壞了,。大家從運(yùn)行結(jié)果上可以看到,我們使用cout<<test1時(shí),,一點(diǎn)反應(yīng)也沒有,。而在test1的析構(gòu)函數(shù)被調(diào)用時(shí),,顯示是這樣:“這個(gè)字符串將被刪除:”。
再看看這段代碼:
cout<<"使用錯(cuò)誤的函數(shù):"<<endl;
show_String(test2);
cout<<test2<<endl;//這一段代碼出現(xiàn)嚴(yán)重的錯(cuò)誤,!
show_String函數(shù)的參數(shù)列表void show_String(const String a)是按值傳遞的,,所以,我們相當(dāng)于執(zhí)行了這樣的代碼:String a=test2;函數(shù)執(zhí)行完畢,,由于生存周期的緣故,,對(duì)象a被析構(gòu)函數(shù)刪除,我們馬上就可以看到錯(cuò)誤的顯示結(jié)果了:這個(gè)字符串將被刪除:?=,。當(dāng)然,,test2也被破壞了。解決的辦法很簡單,,當(dāng)然是手工定義一個(gè)復(fù)制構(gòu)造函數(shù)嘍,!人力可以勝天,!
String::String(const String& a)
{
len=a.len;
str=new char(len+1);
strcpy(str,a.str);
}
我們執(zhí)行的是深復(fù)制,。這個(gè)函數(shù)的功能是這樣的:假設(shè)對(duì)象A中的str指針指向地址2000,內(nèi)容為“I am a C++ Boy!”,。我們執(zhí)行代碼String B=A時(shí),,我們先開辟出一塊內(nèi)存,假設(shè)為3000,。我們用strcpy函數(shù)將地址2000的內(nèi)容拷貝到地址3000中,,再將對(duì)象B的str指針指向地址3000。這樣,,就互不干擾了,。
大家把這個(gè)函數(shù)加入程序中,問題就解決了大半,,但還沒有完全解決,,問題在賦值函數(shù)上。我們的程序中有這樣的段代碼:
String String3;
String3=test4;
經(jīng)過我前面的講解,,大家應(yīng)該也會(huì)對(duì)這段代碼進(jìn)行尋根摸底:憑什么可以這樣做:String3=test4,??,?原因是,,C++為了用戶的方便,提供的這樣的一個(gè)操作符重載函數(shù):operator=,。所以,,我們可以這樣做。大家應(yīng)該猜得到,,它同樣是執(zhí)行了淺復(fù)制,,出了同樣的毛病,。比如,執(zhí)行了這段代碼后,,析構(gòu)函數(shù)開始大展神威^_^,。由于這些變量是后進(jìn)先出的,所以最后的String3變量先被刪除:這個(gè)字符串將被刪除:第四個(gè)范例,。很正常,。最后,刪除到test4的時(shí)候,,問題來了:這個(gè)字符串將被刪除:?=,。原因我不用贅述了,只是這個(gè)賦值函數(shù)怎么寫,,還有一點(diǎn)兒學(xué)問呢,!大家請(qǐng)看:
平時(shí),我們可以寫這樣的代碼:x=y=z,。(均為整型變量,。)而在類對(duì)象中,我們同樣要這樣,,因?yàn)檫@很方便,。而對(duì)象A=B=C就是A.operator=(B.operator=(c))。而這個(gè)operator=函數(shù)的參數(shù)列表應(yīng)該是:const String& a,,所以,,大家不難推出,要實(shí)現(xiàn)這樣的功能,,返回值也要是String&,,這樣才能實(shí)現(xiàn)A=B=C。我們先來寫寫看:
String& String::operator=(const String& a)
{
delete [] str;//先刪除自身的數(shù)據(jù)
len=a.len;
str=new char[len+1];
strcpy(str,a.str);//此三行為進(jìn)行拷貝
return *this;//返回自身的引用
}
是不是這樣就行了呢,?我們假如寫出了這種代碼:A=A,,那么大家看看,豈不是把A對(duì)象的數(shù)據(jù)給刪除了嗎,?這樣可謂引發(fā)一系列的錯(cuò)誤,。所以,我們還要檢查是否為自身賦值,。只比較兩對(duì)象的數(shù)據(jù)是不行了,,因?yàn)閮蓚€(gè)對(duì)象的數(shù)據(jù)很有可能相同。我們應(yīng)該比較地址,。以下是完好的賦值函數(shù):
String& String::operator=(const String& a)
{
if(this==&a)
return *this;
delete [] str;
len=a.len;
str=new char[len+1];
strcpy(str,a.str);
return *this;
}
把這些代碼加入程序,,問題就完全解決,下面是運(yùn)行結(jié)果:
下面分別輸入三個(gè)范例:
第一個(gè)范例
第二個(gè)范例
第三個(gè)范例
第一個(gè)范例
這個(gè)字符串將被刪除:第一個(gè)范例。
第一個(gè)范例
使用正確的函數(shù):
第二個(gè)范例,。
第二個(gè)范例,。
使用錯(cuò)誤的函數(shù):
第二個(gè)范例。
這個(gè)字符串將被刪除:第二個(gè)范例,。
第二個(gè)范例,。
String2: 第三個(gè)范例。
String3: 第四個(gè)范例,。
下面,,程序結(jié)束,析構(gòu)函數(shù)將被調(diào)用,。
這個(gè)字符串將被刪除:第四個(gè)范例,。
這個(gè)字符串將被刪除:第三個(gè)范例。
這個(gè)字符串將被刪除:第四個(gè)范例,。
這個(gè)字符串將被刪除:第三個(gè)范例,。
這個(gè)字符串將被刪除:第二個(gè)范例。
這個(gè)字符串將被刪除:第一個(gè)范例,。
2.2 如何對(duì)付內(nèi)存泄漏,?
寫出那些不會(huì)導(dǎo)致任何內(nèi)存泄漏的代碼。很明顯,,當(dāng)你的代碼中到處充滿了new 操作,、delete操作和指針運(yùn)算的話,你將會(huì)在某個(gè)地方搞暈了頭,,導(dǎo)致內(nèi)存泄漏,指針引用錯(cuò)誤,,以及諸如此類的問題,。這和你如何小心地對(duì)待內(nèi)存分配工作其實(shí)完全沒有關(guān)系:代碼的復(fù)雜性最終總是會(huì)超過你能夠付出的時(shí)間和努力。于是隨后產(chǎn)生了一些成功的技巧,,它們依賴于將內(nèi)存分配(allocations)與重新分配(deallocation)工作隱藏在易于管理的類型之后,。標(biāo)準(zhǔn)容器(standard containers)是一個(gè)優(yōu)秀的例子。它們不是通過你而是自己為元素管理內(nèi)存,,從而避免了產(chǎn)生糟糕的結(jié)果,。想象一下,沒有string和vector的幫助,,寫出這個(gè):
#include<vector>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;
int main() // small program messing around with strings
{
cout << "enter some whitespace-separated words:\n";
vector<string> v;
string s;
while (cin>>s) v.push_back(s);
sort(v.begin(),v.end());
string cat;
typedef vector<string>::const_iterator Iter;
for (Iter p = v.begin(); p!=v.end(); ++p) cat += *p+"+";
cout << cat << ’\n’;
}
你有多少機(jī)會(huì)在第一次就得到正確的結(jié)果,?你又怎么知道你沒有導(dǎo)致內(nèi)存泄漏呢?
注意,,沒有出現(xiàn)顯式的內(nèi)存管理,,宏,造型,溢出檢查,,顯式的長度限制,,以及指針。通過使用函數(shù)對(duì)象和標(biāo)準(zhǔn)算法(standard algorithm),,我可以避免使用指針——例如使用迭代子(iterator),,不過對(duì)于一個(gè)這么小的程序來說有點(diǎn)小題大作了。
這些技巧并不完美,,要系統(tǒng)化地使用它們也并不總是那么容易,。但是,應(yīng)用它們產(chǎn)生了驚人的差異,,而且通過減少顯式的內(nèi)存分配與重新分配的次數(shù),,你甚至可以使余下的例子更加容易被跟蹤。早在1981年,,我就指出,,通過將我必須顯式地跟蹤的對(duì)象的數(shù)量從幾萬個(gè)減少到幾打,為了使程序正確運(yùn)行而付出的努力從可怕的苦工,,變成了應(yīng)付一些可管理的對(duì)象,,甚至更加簡單了。
如果你的程序還沒有包含將顯式內(nèi)存管理減少到最小限度的庫,,那么要讓你程序完成和正確運(yùn)行的話,,最快的途徑也許就是先建立一個(gè)這樣的庫。
模板和標(biāo)準(zhǔn)庫實(shí)現(xiàn)了容器,、資源句柄以及諸如此類的東西,,更早的使用甚至在多年以前。異常的使用使之更加完善,。
如果你實(shí)在不能將內(nèi)存分配/重新分配的操作隱藏到你需要的對(duì)象中時(shí),,你可以使用資源句柄(resource handle),以將內(nèi)存泄漏的可能性降至最低,。這里有個(gè)例子:我需要通過一個(gè)函數(shù),,在空閑內(nèi)存中建立一個(gè)對(duì)象并返回它。這時(shí)候可能忘記釋放這個(gè)對(duì)象,。畢竟,,我們不能說,僅僅關(guān)注當(dāng)這個(gè)指針要被釋放的時(shí)候,,誰將負(fù)責(zé)去做,。使用資源句柄,這里用了標(biāo)準(zhǔn)庫中的auto_ptr,,使需要為之負(fù)責(zé)的地方變得明確了,。
#include<memory>
#include<iostream>
using namespace std;
struct S {
S() { cout << "make an S\n"; }
~S() { cout << "destroy an S\n"; }
S(const S&) { cout << "copy initialize an S\n"; }
S& operator=(const S&) { cout << "copy assign an S\n"; }
};
S* f()
{
return new S; // 誰該負(fù)責(zé)釋放這個(gè)S?
};
auto_ptr<S> g()
{
return auto_ptr<S>(new S); // 顯式傳遞負(fù)責(zé)釋放這個(gè)S
}
int main()
{
cout << "start main\n";
S* p = f();
cout << "after f() before g()\n";
// S* q = g(); // 將被編譯器捕捉
auto_ptr<S> q = g();
cout << "exit main\n";
// *p產(chǎn)生了內(nèi)存泄漏
// *q被自動(dòng)釋放
}
在更一般的意義上考慮資源,而不僅僅是內(nèi)存,。
如果在你的環(huán)境中不能系統(tǒng)地應(yīng)用這些技巧(例如,,你必須使用別的地方的代碼,或者你的程序的另一部分簡直是原始人類(譯注:原文是Neanderthals,,尼安德特人,,舊石器時(shí)代廣泛分布在歐洲的猿人)寫的,如此等等),,那么注意使用一個(gè)內(nèi)存泄漏檢測器作為開發(fā)過程的一部分,,或者插入一個(gè)垃圾收集器(garbage collector)。
2.3淺談C/C++內(nèi)存泄漏及其檢測工具
對(duì)于一個(gè)c/c++程序員來說,,內(nèi)存泄漏是一個(gè)常見的也是令人頭疼的問題,。已經(jīng)有許多技術(shù)被研究出來以應(yīng)對(duì)這個(gè)問題,比如Smart Pointer,,Garbage Collection等,。Smart Pointer技術(shù)比較成熟,STL中已經(jīng)包含支持Smart Pointer的class,,但是它的使用似乎并不廣泛,,而且它也不能解決所有的問題;Garbage Collection技術(shù)在Java中已經(jīng)比較成熟,,但是在c/c++領(lǐng)域的發(fā)展并不順暢,,雖然很早就有人思考在C++中也加入GC的支持。現(xiàn)實(shí)世界就是這樣的,,作為一個(gè)c/c++程序員,,內(nèi)存泄漏是你心中永遠(yuǎn)的痛。不過好在現(xiàn)在有許多工具能夠幫助我們驗(yàn)證內(nèi)存泄漏的存在,,找出發(fā)生問題的代碼,。
2.3.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()返回零的時(shí)候,指針p指向的內(nèi)存就不會(huì)被釋放,。這是一種常見的發(fā)生內(nèi)存泄漏的情形,。程序在入口處分配內(nèi)存,在出口處釋放內(nèi)存,,但是c函數(shù)可以在任何地方退出,,所以一旦有某個(gè)出口處沒有釋放應(yīng)該釋放的內(nèi)存,就會(huì)發(fā)生內(nèi)存泄漏,。
廣義的說,,內(nèi)存泄漏不僅僅包含堆內(nèi)存的泄漏,還包含系統(tǒng)資源的泄漏(resource leak),,比如核心態(tài)HANDLE,,GDI Object,SOCKET,, Interface等,,從根本上說這些由操作系統(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的泄漏是一種常見的資源泄漏:
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í)候,,程序在退出前沒有把pOldBmp選回pDC中,,這會(huì)導(dǎo)致pOldBmp指向的HBITMAP對(duì)象發(fā)生泄漏。這個(gè)程序如果長時(shí)間的運(yùn)行,,可能會(huì)導(dǎo)致整個(gè)系統(tǒng)花屏,。這種問題在Win9x下比較容易暴露出來,因?yàn)閃in9x的GDI堆比Win2k或NT的要小很多,。
2.3.2 內(nèi)存泄漏的發(fā)生方式
以發(fā)生的方式來分類,,內(nèi)存泄漏可以分為4類:
1. 常發(fā)性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼會(huì)被多次執(zhí)行到,,每次被執(zhí)行的時(shí)候都會(huì)導(dǎo)致一塊內(nèi)存泄漏,。比如例二,如果Something()函數(shù)一直返回True,,那么pOldBmp指向的HBITMAP對(duì)象總是發(fā)生泄漏,。
2. 偶發(fā)性內(nèi)存泄漏。發(fā)生內(nèi)存泄漏的代碼只有在某些特定環(huán)境或操作過程下才會(huì)發(fā)生,。比如例二,,如果Something()函數(shù)只有在特定環(huán)境下才返回True,,那么pOldBmp指向的HBITMAP對(duì)象并不總是發(fā)生泄漏。常發(fā)性和偶發(fā)性是相對(duì)的,。對(duì)于特定的環(huán)境,,偶發(fā)性的也許就變成了常發(fā)性的。所以測試環(huán)境和測試方法對(duì)檢測內(nèi)存泄漏至關(guān)重要,。
3. 一次性內(nèi)存泄漏,。發(fā)生內(nèi)存泄漏的代碼只會(huì)被執(zhí)行一次,或者由于算法上的缺陷,,導(dǎo)致總會(huì)有一塊僅且一塊內(nèi)存發(fā)生泄漏,。比如,在類的構(gòu)造函數(shù)中分配內(nèi)存,,在析構(gòu)函數(shù)中卻沒有釋放該內(nèi)存,,但是因?yàn)檫@個(gè)類是一個(gè)Singleton,所以內(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í)候沒有釋放g_lpszFileName指向的字符串,,那么,即使多次調(diào)用SetFileName(),,總會(huì)有一塊內(nèi)存,,而且僅有一塊內(nèi)存發(fā)生泄漏。
4. 隱式內(nèi)存泄漏,。程序在運(yùn)行過程中不停的分配內(nèi)存,,但是直到結(jié)束的時(shí)候才釋放內(nèi)存。嚴(yán)格的說這里并沒有發(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)存。所以,,我們稱這類內(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端斷開后,Server并沒有呼叫OnClientDisconnected()函數(shù),,那么代表那次連接的Connection對(duì)象就不會(huì)被及時(shí)的刪除(在Server程序退出的時(shí)候,,所有Connection對(duì)象會(huì)在ConnectionManager的析構(gòu)函數(shù)里被刪除)。當(dāng)不斷的有連接建立,、斷開時(shí)隱式內(nèi)存泄漏就發(fā)生了。
從用戶使用程序的角度來看,,內(nèi)存泄漏本身不會(huì)產(chǎn)生什么危害,,作為一般的用戶,,根本感覺不到內(nèi)存泄漏的存在。真正有危害的是內(nèi)存泄漏的堆積,,這會(huì)最終消耗盡系統(tǒng)所有的內(nèi)存,。從這個(gè)角度來說,一次性內(nèi)存泄漏并沒有什么危害,,因?yàn)樗粫?huì)堆積,,而隱式內(nèi)存泄漏危害性則非常大,因?yàn)檩^之于常發(fā)性和偶發(fā)性內(nèi)存泄漏它更難被檢測到,。
2.3.3 檢測內(nèi)存泄漏
檢測內(nèi)存泄漏的關(guān)鍵是要能截獲住對(duì)分配內(nèi)存和釋放內(nèi)存的函數(shù)的調(diào)用,。截獲住這兩個(gè)函數(shù),我們就能跟蹤每一塊內(nèi)存的生命周期,,比如,,每當(dāng)成功的分配一塊內(nèi)存后,就把它的指針加入一個(gè)全局的list中,;每當(dāng)釋放一塊內(nèi)存,,再把它的指針從list中刪除。這樣,,當(dāng)程序結(jié)束的時(shí)候,,list中剩余的指針就是指向那些沒有被釋放的內(nèi)存。這里只是簡單的描述了檢測內(nèi)存泄漏的基本原理,,詳細(xì)的算法可以參見Steve Maguire的<<Writing Solid Code>>,。
如果要檢測堆內(nèi)存的泄漏,那么需要截獲住malloc/realloc/free和new/delete就可以了(其實(shí)new/delete最終也是用malloc/free的,,所以只要截獲前面一組即可),。對(duì)于其他的泄漏,可以采用類似的方法,,截獲住相應(yīng)的分配和釋放函數(shù),。比如,要檢測BSTR的泄漏,,就需要截獲SysAllocString/SysFreeString,;要檢測HMENU的泄漏,就需要截獲CreateMenu/ DestroyMenu,。(有的資源的分配函數(shù)有多個(gè),,釋放函數(shù)只有一個(gè),比如,,SysAllocStringLen也可以用來分配BSTR,,這時(shí)就需要截獲多個(gè)分配函數(shù))
在Windows平臺(tái)下,檢測內(nèi)存泄漏的工具常用的一般有三種,,MS C-Runtime Library內(nèi)建的檢測功能,;外掛式的檢測工具,,諸如,Purify,,BoundsChecker等,;利用Windows NT自帶的Performance Monitor。這三種工具各有優(yōu)缺點(diǎn),,MS C-Runtime Library雖然功能上較之外掛式的工具要弱,,但是它是免費(fèi)的;Performance Monitor雖然無法標(biāo)示出發(fā)生問題的代碼,,但是它能檢測出隱式的內(nèi)存泄漏的存在,,這是其他兩類工具無能為力的地方。
以下我們?cè)敿?xì)討論這三種檢測工具:
2.3.3.1 VC下內(nèi)存泄漏的檢測方法
用MFC開發(fā)的應(yīng)用程序,,在DEBUG版模式下編譯后,,都會(huì)自動(dòng)加入內(nèi)存泄漏的檢測代碼。在程序結(jié)束后,,如果發(fā)生了內(nèi)存泄漏,,在Debug窗口中會(huì)顯示出所有發(fā)生泄漏的內(nèi)存塊的信息,以下兩行顯示了一塊被泄漏的內(nèi)存塊的信息:
E:\TestMemLeak\TestDlg.cpp(70) : {59} normal block at 0x00881710, 200 bytes long.
Data: <abcdefghijklmnop> 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70
第一行顯示該內(nèi)存塊由TestDlg.cpp文件,,第70行代碼分配,,地址在0x00881710,大小為200字節(jié),,{59}是指調(diào)用內(nèi)存分配函數(shù)的Request Order,,關(guān)于它的詳細(xì)信息可以參見MSDN中_CrtSetBreakAlloc()的幫助。第二行顯示該內(nèi)存塊前16個(gè)字節(jié)的內(nèi)容,,尖括號(hào)內(nèi)是以ASCII方式顯示,,接著的是以16進(jìn)制方式顯示。
一般大家都誤以為這些內(nèi)存泄漏的檢測功能是由MFC提供的,,其實(shí)不然,。MFC只是封裝和利用了MS C-Runtime Library的Debug Function。非MFC程序也可以利用MS C-Runtime Library的Debug Function加入內(nèi)存泄漏的檢測功能,。MS C-Runtime Library在實(shí)現(xiàn)malloc/free,,strdup等函數(shù)時(shí)已經(jīng)內(nèi)建了內(nèi)存泄漏的檢測功能。
注意觀察一下由MFC Application Wizard生成的項(xiàng)目,,在每一個(gè)cpp文件的頭部都有這樣一段宏定義:
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
有了這樣的定義,,在編譯DEBUG版時(shí),出現(xiàn)在這個(gè)cpp文件中的所有new都被替換成DEBUG_NEW了,。那么DEBUG_NEW是什么呢,?DEBUG_NEW也是一個(gè)宏,以下摘自afx.h,,1632行
#define DEBUG_NEW new(THIS_FILE, __LINE__)
所以如果有這樣一行代碼:
char* p = new char[200];
經(jīng)過宏替換就變成了:
char* p = new( THIS_FILE, __LINE__)char[200];
根據(jù)C++的標(biāo)準(zhǔn),,對(duì)于以上的new的使用方法,,編譯器會(huì)去找這樣定義的operator new:
void* operator new(size_t, LPCSTR, int)
我們?cè)赼fxmem.cpp 63行找到了一個(gè)這樣的operator new 的實(shí)現(xiàn)
void* AFX_CDECL operator new(size_t nSize, LPCSTR lpszFileName, int nLine)
{
return ::operator new(nSize, _NORMAL_BLOCK, lpszFileName, nLine);
}
void* __cdecl operator new(size_t nSize, int nType, LPCSTR lpszFileName, int nLine)
{
…
pResult = _malloc_dbg(nSize, nType, lpszFileName, nLine);
if (pResult != NULL)
return pResult;
…
}
第二個(gè)operator new函數(shù)比較長,為了簡單期間,,我只摘錄了部分。很顯然最后的內(nèi)存分配還是通過_malloc_dbg函數(shù)實(shí)現(xiàn)的,,這個(gè)函數(shù)屬于MS C-Runtime Library 的Debug Function,。這個(gè)函數(shù)不但要求傳入內(nèi)存的大小,另外還有文件名和行號(hào)兩個(gè)參數(shù),。文件名和行號(hào)就是用來記錄此次分配是由哪一段代碼造成的,。如果這塊內(nèi)存在程序結(jié)束之前沒有被釋放,那么這些信息就會(huì)輸出到Debug窗口里,。
這里順便提一下THIS_FILE,,__FILE和__LINE__。__FILE__和__LINE__都是編譯器定義的宏,。當(dāng)碰到__FILE__時(shí),,編譯器會(huì)把__FILE__替換成一個(gè)字符串,這個(gè)字符串就是當(dāng)前在編譯的文件的路徑名,。當(dāng)碰到__LINE__時(shí),,編譯器會(huì)把__LINE__替換成一個(gè)數(shù)字,這個(gè)數(shù)字就是當(dāng)前這行代碼的行號(hào),。在DEBUG_NEW的定義中沒有直接使用__FILE__,,而是用了THIS_FILE,其目的是為了減小目標(biāo)文件的大小,。假設(shè)在某個(gè)cpp文件中有100處使用了new,,如果直接使用__FILE__,那編譯器會(huì)產(chǎn)生100個(gè)常量字符串,,這100個(gè)字符串都是飧?/SPAN>cpp文件的路徑名,,顯然十分冗余。如果使用THIS_FILE,,編譯器只會(huì)產(chǎn)生一個(gè)常量字符串,,那100處new的調(diào)用使用的都是指向常量字符串的指針。
再次觀察一下由MFC Application Wizard生成的項(xiàng)目,,我們會(huì)發(fā)現(xiàn)在cpp文件中只對(duì)new做了映射,,如果你在程序中直接使用malloc函數(shù)分配內(nèi)存,調(diào)用malloc的文件名和行號(hào)是不會(huì)被記錄下來的,。如果這塊內(nèi)存發(fā)生了泄漏,,MS C-Runtime Library仍然能檢測到,但是當(dāng)輸出這塊內(nèi)存塊的信息,,不會(huì)包含分配它的的文件名和行號(hào),。
要在非MFC程序中打開內(nèi)存泄漏的檢測功能非常容易,,你只要在程序的入口處加入以下幾行代碼:
int tmpFlag = _CrtSetDbgFlag( _CRTDBG_REPORT_FLAG );
tmpFlag |= _CRTDBG_LEAK_CHECK_DF;
_CrtSetDbgFlag( tmpFlag );
這樣,在程序結(jié)束的時(shí)候,,也就是winmain,,main或dllmain函數(shù)返回之后,如果還有內(nèi)存塊沒有釋放,,它們的信息會(huì)被打印到Debug窗口里,。
如果你試著創(chuàng)建了一個(gè)非MFC應(yīng)用程序,而且在程序的入口處加入了以上代碼,,并且故意在程序中不釋放某些內(nèi)存塊,,你會(huì)在Debug窗口里看到以下的信息:
{47} normal block at 0x00C91C90, 200 bytes long.
Data: < > 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
內(nèi)存泄漏的確檢測到了,但是和上面MFC程序的例子相比,,缺少了文件名和行號(hào),。對(duì)于一個(gè)比較大的程序,沒有這些信息,,解決問題將變得十分困難,。
為了能夠知道泄漏的內(nèi)存塊是在哪里分配的,你需要實(shí)現(xiàn)類似MFC的映射功能,,把new,,maolloc等函數(shù)映射到_malloc_dbg函數(shù)上。這里我不再贅述,,你可以參考MFC的源代碼,。
由于Debug Function實(shí)現(xiàn)在MS C-RuntimeLibrary中,所以它只能檢測到堆內(nèi)存的泄漏,,而且只限于malloc,,realloc或strdup等分配的內(nèi)存,而那些系統(tǒng)資源,,比如HANDLE,,GDI Object,或是不通過C-Runtime Library分配的內(nèi)存,,比如VARIANT,,BSTR的泄漏,它是無法檢測到的,,這是這種檢測法的一個(gè)重大的局限性,。另外,為了能記錄內(nèi)存塊是在哪里分配的,,源代碼必須相應(yīng)的配合,,這在調(diào)試一些老的程序非常麻煩,畢竟修改源代碼不是一件省心的事,這是這種檢測法的另一個(gè)局限性,。
對(duì)于開發(fā)一個(gè)大型的程序,,MS C-Runtime Library提供的檢測功能是遠(yuǎn)遠(yuǎn)不夠的。接下來我們就看看外掛式的檢測工具,。我用的比較多的是BoundsChecker,,一則因?yàn)樗墓δ鼙容^全面,更重要的是它的穩(wěn)定性,。這類工具如果不穩(wěn)定,,反而會(huì)忙里添亂。到底是出自鼎鼎大名的NuMega,,我用下來基本上沒有什么大問題。
2.3.3.2 使用BoundsChecker檢測內(nèi)存泄漏
BoundsChecker采用一種被稱為 Code Injection的技術(shù),,來截獲對(duì)分配內(nèi)存和釋放內(nèi)存的函數(shù)的調(diào)用,。簡單地說,當(dāng)你的程序開始運(yùn)行時(shí),,BoundsChecker的DLL被自動(dòng)載入進(jìn)程的地址空間(這可以通過system-level的Hook實(shí)現(xiàn)),,然后它會(huì)修改進(jìn)程中對(duì)內(nèi)存分配和釋放的函數(shù)調(diào)用,讓這些調(diào)用首先轉(zhuǎn)入它的代碼,,然后再執(zhí)行原來的代碼,。BoundsChecker在做這些動(dòng)作的時(shí),無須修改被調(diào)試程序的源代碼或工程配置文件,,這使得使用它非常的簡便,、直接。
這里我們以malloc函數(shù)為例,,截獲其他的函數(shù)方法與此類似,。
需要被截獲的函數(shù)可能在DLL中,也可能在程序的代碼里,。比如,,如果靜態(tài)連結(jié)C-Runtime Library,那么malloc函數(shù)的代碼會(huì)被連結(jié)到程序里,。為了截獲住對(duì)這類函數(shù)的調(diào)用,,BoundsChecker會(huì)動(dòng)態(tài)修改這些函數(shù)的指令。
以下兩段匯編代碼,,一段沒有BoundsChecker介入,,另一段則有BoundsChecker的介入:
126: _CRTIMP void * __cdecl malloc (
127: size_t nSize
128: )
129: {
00403C10 push ebp
00403C11 mov ebp,esp
130: return _nh_malloc_dbg(nSize, _newmode, _NORMAL_BLOCK, NULL, 0);
00403C13 push 0
00403C15 push 0
00403C17 push 1
00403C19 mov eax,[__newmode (0042376c)]
00403C1E push eax
00403C1F mov ecx,dword ptr [nSize]
00403C22 push ecx
00403C23 call _nh_malloc_dbg (00403c80)
00403C28 add esp,14h
131: }
以下這一段代碼有BoundsChecker介入:
126: _CRTIMP void * __cdecl malloc (
127: size_t nSize
128: )
129: {
00403C10 jmp 01F41EC8
00403C15 push 0
00403C17 push 1
00403C19 mov eax,[__newmode (0042376c)]
00403C1E push eax
00403C1F mov ecx,dword ptr [nSize]
00403C22 push ecx
00403C23 call _nh_malloc_dbg (00403c80)
00403C28 add esp,14h
131: }
當(dāng)BoundsChecker介入后,函數(shù)malloc的前三條匯編指令被替換成一條jmp指令,,原來的三條指令被搬到地址01F41EC8處了,。當(dāng)程序進(jìn)入malloc后先jmp到01F41EC8,執(zhí)行原來的三條指令,然后就是BoundsChecker的天下了,。大致上它會(huì)先記錄函數(shù)的返回地址(函數(shù)的返回地址在stack上,,所以很容易修改),然后把返回地址指向?qū)儆贐oundsChecker的代碼,,接著跳到malloc函數(shù)原來的指令,,也就是在00403c15的地方。當(dāng)malloc函數(shù)結(jié)束的時(shí)候,,由于返回地址被修改,,它會(huì)返回到BoundsChecker的代碼中,此時(shí)BoundsChecker會(huì)記錄由malloc分配的內(nèi)存的指針,,然后再跳轉(zhuǎn)到到原來的返回地址去,。
如果內(nèi)存分配/釋放函數(shù)在DLL中,BoundsChecker則采用另一種方法來截獲對(duì)這些函數(shù)的調(diào)用,。BoundsChecker通過修改程序的DLL Import Table讓table中的函數(shù)地址指向自己的地址,,以達(dá)到截獲的目的。
截獲住這些分配和釋放函數(shù),,BoundsChecker就能記錄被分配的內(nèi)存或資源的生命周期,。接下來的問題是如何與源代碼相關(guān),也就是說當(dāng)BoundsChecker檢測到內(nèi)存泄漏,,它如何報(bào)告這塊內(nèi)存塊是哪段代碼分配的,。答案是調(diào)試信息(Debug Information)。當(dāng)我們編譯一個(gè)Debug版的程序時(shí),,編譯器會(huì)把源代碼和二進(jìn)制代碼之間的對(duì)應(yīng)關(guān)系記錄下來,,放到一個(gè)單獨(dú)的文件里(.pdb)或者直接連結(jié)進(jìn)目標(biāo)程序,通過直接讀取調(diào)試信息就能得到分配某塊內(nèi)存的源代碼在哪個(gè)文件,,哪一行上,。使用Code Injection和Debug Information,使BoundsChecker不但能記錄呼叫分配函數(shù)的源代碼的位置,,而且還能記錄分配時(shí)的Call Stack,,以及Call Stack上的函數(shù)的源代碼位置。這在使用像MFC這樣的類庫時(shí)非常有用,,以下我用一個(gè)例子來說明:
void ShowXItemMenu()
{
…
CMenu menu;
menu.CreatePopupMenu();
//add menu items.
menu.TrackPropupMenu();
…
}
void ShowYItemMenu( )
{
…
CMenu menu;
menu.CreatePopupMenu();
//add menu items.
menu.TrackPropupMenu();
menu.Detach();//this will cause HMENU leak
…
}
BOOL CMenu::CreatePopupMenu()
{
…
hMenu = CreatePopupMenu();
…
}
當(dāng)調(diào)用ShowYItemMenu()時(shí),,我們故意造成HMENU的泄漏。但是,,對(duì)于BoundsChecker來說被泄漏的HMENU是在class CMenu::CreatePopupMenu()中分配的,。假設(shè)的你的程序有許多地方使用了CMenu的CreatePopupMenu()函數(shù),如CMenu::CreatePopupMenu()造成的,,你依然無法確認(rèn)問題的根結(jié)到底在哪里,,在ShowXItemMenu()中還是在ShowYItemMenu()中,,或者還有其它的地方也使用了CreatePopupMenu()?有了Call Stack的信息,,問題就容易了,。BoundsChecker會(huì)如下報(bào)告泄漏的HMENU的信息:
Function
File
Line
CMenu::CreatePopupMenu
E:68\vc98\mfc\mfc\include\afxwin1.inl
1009
ShowYItemMenu
E:\testmemleak\mytest.cpp
100
這里省略了其他的函數(shù)調(diào)用
如此,我們很容易找到發(fā)生問題的函數(shù)是ShowYItemMenu(),。當(dāng)使用MFC之類的類庫編程時(shí),,大部分的API調(diào)用都被封裝在類庫的class里,有了Call Stack信息,,我們就可以非常容易的追蹤到真正發(fā)生泄漏的代碼,。
記錄Call Stack信息會(huì)使程序的運(yùn)行變得非常慢,因此默認(rèn)情況下BoundsChecker不會(huì)記錄Call Stack信息,??梢园凑找韵碌牟襟E打開記錄Call Stack信息的選項(xiàng)開關(guān):
1. 打開菜單:BoundsChecker|Setting…
2. 在Error Detection頁中,在Error Detection Scheme的List中選擇Custom
3. 在Category的Combox中選擇 Pointer and leak error check
4. 鉤上Report Call Stack復(fù)選框
5. 點(diǎn)擊Ok
基于Code Injection,,BoundsChecker還提供了API Parameter的校驗(yàn)功能,,memory over run等功能。這些功能對(duì)于程序的開發(fā)都非常有益,。由于這些內(nèi)容不屬于本文的主題,所以不在此詳述了,。
盡管BoundsChecker的功能如此強(qiáng)大,,但是面對(duì)隱式內(nèi)存泄漏仍然顯得蒼白無力。所以接下來我們看看如何用Performance Monitor檢測內(nèi)存泄漏,。
2.3.3.3 使用Performance Monitor檢測內(nèi)存泄漏
NT的內(nèi)核在設(shè)計(jì)過程中已經(jīng)加入了系統(tǒng)監(jiān)視功能,,比如CPU的使用率,內(nèi)存的使用情況,,I/O操作的頻繁度等都作為一個(gè)個(gè)Counter,,應(yīng)用程序可以通過讀取這些Counter了解整個(gè)系統(tǒng)的或者某個(gè)進(jìn)程的運(yùn)行狀況。Performance Monitor就是這樣一個(gè)應(yīng)用程序,。
為了檢測內(nèi)存泄漏,,我們一般可以監(jiān)視Process對(duì)象的Handle Count,Virutal Bytes 和Working Set三個(gè)Counter,。Handle Count記錄了進(jìn)程當(dāng)前打開的HANDLE的個(gè)數(shù),,監(jiān)視這個(gè)Counter有助于我們發(fā)現(xiàn)程序是否有Handle泄漏;Virtual Bytes記錄了該進(jìn)程當(dāng)前在虛地址空間上使用的虛擬內(nèi)存的大小,,NT的內(nèi)存分配采用了兩步走的方法,,首先,在虛地址空間上保留一段空間,,這時(shí)操作系統(tǒng)并沒有分配物理內(nèi)存,,只是保留了一段地址。然后,再提交這段空間,,這時(shí)操作系統(tǒng)才會(huì)分配物理內(nèi)存,。所以,Virtual Bytes一般總大于程序的Working Set,。監(jiān)視Virutal Bytes可以幫助我們發(fā)現(xiàn)一些系統(tǒng)底層的問題; Working Set記錄了操作系統(tǒng)為進(jìn)程已提交的內(nèi)存的總量,,這個(gè)值和程序申請(qǐng)的內(nèi)存總量存在密切的關(guān)系,如果程序存在內(nèi)存的泄漏這個(gè)值會(huì)持續(xù)增加,,但是Virtual Bytes卻是跳躍式增加的,。
監(jiān)視這些Counter可以讓我們了解進(jìn)程使用內(nèi)存的情況,如果發(fā)生了泄漏,,即使是隱式內(nèi)存泄漏,,這些Counter的值也會(huì)持續(xù)增加。但是,,我們知道有問題卻不知道哪里有問題,,所以一般使用Performance Monitor來驗(yàn)證是否有內(nèi)存泄漏,而使用BoundsChecker來找到和解決,。
當(dāng)Performance Monitor顯示有內(nèi)存泄漏,,而BoundsChecker卻無法檢測到,這時(shí)有兩種可能:第一種,,發(fā)生了偶發(fā)性內(nèi)存泄漏,。這時(shí)你要確保使用Performance Monitor和使用BoundsChecker時(shí),程序的運(yùn)行環(huán)境和操作方法是一致的,。第二種,,發(fā)生了隱式的內(nèi)存泄漏。這時(shí)你要重新審查程序的設(shè)計(jì),,然后仔細(xì)研究Performance Monitor記錄的Counter的值的變化圖,,分析其中的變化和程序運(yùn)行邏輯的關(guān)系,找到一些可能的原因,。這是一個(gè)痛苦的過程,,充滿了假設(shè)、猜想,、驗(yàn)證,、失敗,但這也是一個(gè)積累經(jīng)驗(yàn)的絕好機(jī)會(huì),。
3 探討C++內(nèi)存回收
3.1 C++內(nèi)存對(duì)象大會(huì)戰(zhàn)
如果一個(gè)人自稱為程序高手,,卻對(duì)內(nèi)存一無所知,那么我可以告訴你,,他一定在吹牛,。用C或C++寫程序,,需要更多地關(guān)注內(nèi)存,這不僅僅是因?yàn)閮?nèi)存的分配是否合理直接影響著程序的效率和性能,,更為主要的是,,當(dāng)我們操作內(nèi)存的時(shí)候一不小心就會(huì)出現(xiàn)問題,而且很多時(shí)候,,這些問題都是不易發(fā)覺的,,比如內(nèi)存泄漏,比如懸掛指針,。筆者今天在這里并不是要討論如何避免這些問題,,而是想從另外一個(gè)角度來認(rèn)識(shí)C++內(nèi)存對(duì)象。
我們知道,,C++將內(nèi)存劃分為三個(gè)邏輯區(qū)域:堆,、棧和靜態(tài)存儲(chǔ)區(qū)。既然如此,,我稱位于它們之中的對(duì)象分別為堆對(duì)象,,棧對(duì)象以及靜態(tài)對(duì)象。那么這些不同的內(nèi)存對(duì)象有什么區(qū)別了,?堆對(duì)象和棧對(duì)象各有什么優(yōu)劣了,?如何禁止創(chuàng)建堆對(duì)象或棧對(duì)象了?這些便是今天的主題,。
3.1.1 基本概念
先來看看棧,。棧,一般用于存放局部變量或?qū)ο?,如我們?cè)诤瘮?shù)定義中用類似下面語句聲明的對(duì)象:
Type stack_object ;
stack_object便是一個(gè)棧對(duì)象,它的生命期是從定義點(diǎn)開始,,當(dāng)所在函數(shù)返回時(shí),,生命結(jié)束。
另外,,幾乎所有的臨時(shí)對(duì)象都是棧對(duì)象,。比如,下面的函數(shù)定義:
Type fun(Type object);
這個(gè)函數(shù)至少產(chǎn)生兩個(gè)臨時(shí)對(duì)象,,首先,,參數(shù)是按值傳遞的,所以會(huì)調(diào)用拷貝構(gòu)造函數(shù)生成一個(gè)臨時(shí)對(duì)象object_copy1 ,,在函數(shù)內(nèi)部使用的不是使用的不是object,,而是object_copy1,自然,,object_copy1是一個(gè)棧對(duì)象,,它在函數(shù)返回時(shí)被釋放,;還有這個(gè)函數(shù)是值返回的,在函數(shù)返回時(shí),,如果我們不考慮返回值優(yōu)化(NRV),,那么也會(huì)產(chǎn)生一個(gè)臨時(shí)對(duì)象object_copy2,這個(gè)臨時(shí)對(duì)象會(huì)在函數(shù)返回后一段時(shí)間內(nèi)被釋放,。比如某個(gè)函數(shù)中有如下代碼:
Type tt ,result ; //生成兩個(gè)棧對(duì)象
tt = fun(tt); //函數(shù)返回時(shí),,生成的是一個(gè)臨時(shí)對(duì)象object_copy2
上面的第二個(gè)語句的執(zhí)行情況是這樣的,首先函數(shù)fun返回時(shí)生成一個(gè)臨時(shí)對(duì)象object_copy2 ,,然后再調(diào)用賦值運(yùn)算符執(zhí)行
tt = object_copy2 ; //調(diào)用賦值運(yùn)算符
看到了嗎,?編譯器在我們毫無知覺的情況下,為我們生成了這么多臨時(shí)對(duì)象,,而生成這些臨時(shí)對(duì)象的時(shí)間和空間的開銷可能是很大的,,所以,你也許明白了,,為什么對(duì)于“大”對(duì)象最好用const引用傳遞代替按值進(jìn)行函數(shù)參數(shù)傳遞了,。
接下來,看看堆,。堆,,又叫自由存儲(chǔ)區(qū),它是在程序執(zhí)行的過程中動(dòng)態(tài)分配的,,所以它最大的特性就是動(dòng)態(tài)性,。在C++中,所有堆對(duì)象的創(chuàng)建和銷毀都要由程序員負(fù)責(zé),,所以,,如果處理不好,就會(huì)發(fā)生內(nèi)存問題,。如果分配了堆對(duì)象,,卻忘記了釋放,就會(huì)產(chǎn)生內(nèi)存泄漏,;而如果已釋放了對(duì)象,,卻沒有將相應(yīng)的指針置為NULL,該指針就是所謂的“懸掛指針”,,再度使用此指針時(shí),,就會(huì)出現(xiàn)非法訪問,嚴(yán)重時(shí)就導(dǎo)致程序崩潰,。
那么,,C++中是怎樣分配堆對(duì)象的?唯一的方法就是用new(當(dāng)然,,用類malloc指令也可獲得C式堆內(nèi)存),,只要使用new,,就會(huì)在堆中分配一塊內(nèi)存,并且返回指向該堆對(duì)象的指針,。
再來看看靜態(tài)存儲(chǔ)區(qū),。所有的靜態(tài)對(duì)象、全局對(duì)象都于靜態(tài)存儲(chǔ)區(qū)分配,。關(guān)于全局對(duì)象,,是在main()函數(shù)執(zhí)行前就分配好了的。其實(shí),,在main()函數(shù)中的顯示代碼執(zhí)行之前,,會(huì)調(diào)用一個(gè)由編譯器生成的_main()函數(shù),而_main()函數(shù)會(huì)進(jìn)行所有全局對(duì)象的的構(gòu)造及初始化工作,。而在main()函數(shù)結(jié)束之前,,會(huì)調(diào)用由編譯器生成的exit函數(shù),來釋放所有的全局對(duì)象,。比如下面的代碼:
void main(void)
{
… …// 顯式代碼
}
實(shí)際上,,被轉(zhuǎn)化成這樣:
void main(void)
{
_main(); //隱式代碼,由編譯器產(chǎn)生,,用以構(gòu)造所有全局對(duì)象
… … // 顯式代碼
… …
exit() ; // 隱式代碼,,由編譯器產(chǎn)生,用以釋放所有全局對(duì)象
}
所以,,知道了這個(gè)之后,,便可以由此引出一些技巧,如,,假設(shè)我們要在main()函數(shù)執(zhí)行之前做某些準(zhǔn)備工作,,那么我們可以將這些準(zhǔn)備工作寫到一個(gè)自定義的全局對(duì)象的構(gòu)造函數(shù)中,這樣,,在main()函數(shù)的顯式代碼執(zhí)行之前,,這個(gè)全局對(duì)象的構(gòu)造函數(shù)會(huì)被調(diào)用,執(zhí)行預(yù)期的動(dòng)作,,這樣就達(dá)到了我們的目的。 剛才講的是靜態(tài)存儲(chǔ)區(qū)中的全局對(duì)象,,那么,,局部靜態(tài)對(duì)象了?局部靜態(tài)對(duì)象通常也是在函數(shù)中定義的,,就像棧對(duì)象一樣,,只不過,其前面多了個(gè)static關(guān)鍵字,。局部靜態(tài)對(duì)象的生命期是從其所在函數(shù)第一次被調(diào)用,,更確切地說,,是當(dāng)?shù)谝淮螆?zhí)行到該靜態(tài)對(duì)象的聲明代碼時(shí),產(chǎn)生該靜態(tài)局部對(duì)象,,直到整個(gè)程序結(jié)束時(shí),,才銷毀該對(duì)象。
還有一種靜態(tài)對(duì)象,,那就是它作為class的靜態(tài)成員,。考慮這種情況時(shí),,就牽涉了一些較復(fù)雜的問題,。
第一個(gè)問題是class的靜態(tài)成員對(duì)象的生命期,class的靜態(tài)成員對(duì)象隨著第一個(gè)class object的產(chǎn)生而產(chǎn)生,,在整個(gè)程序結(jié)束時(shí)消亡,。也就是有這樣的情況存在,在程序中我們定義了一個(gè)class,,該類中有一個(gè)靜態(tài)對(duì)象作為成員,,但是在程序執(zhí)行過程中,如果我們沒有創(chuàng)建任何一個(gè)該class object,,那么也就不會(huì)產(chǎn)生該class所包含的那個(gè)靜態(tài)對(duì)象,。還有,如果創(chuàng)建了多個(gè)class object,,那么所有這些object都共享那個(gè)靜態(tài)對(duì)象成員,。
第二個(gè)問題是,當(dāng)出現(xiàn)下列情況時(shí):
class Base
{
public:
static Type s_object ;
}
class Derived1 : public Base / / 公共繼承
{
… …// other data
}
class Derived2 : public Base / / 公共繼承
{
… …// other data
}
Base example ;
Derivde1 example1 ;
Derivde2 example2 ;
example.s_object = …… ;
example1.s_object = …… ;
example2.s_object = …… ;
請(qǐng)注意上面標(biāo)為黑體的三條語句,,它們所訪問的s_object是同一個(gè)對(duì)象嗎,?答案是肯定的,它們的確是指向同一個(gè)對(duì)象,,這聽起來不像是真的,,是嗎?但這是事實(shí),,你可以自己寫段簡單的代碼驗(yàn)證一下,。我要做的是來解釋為什么會(huì)這樣? 我們知道,,當(dāng)一個(gè)類比如Derived1,,從另一個(gè)類比如Base繼承時(shí),,那么,,可以看作一個(gè)Derived1對(duì)象中含有一個(gè)Base型的對(duì)象,,這就是一個(gè)subobject。一個(gè)Derived1對(duì)象的大致內(nèi)存布局如下:
讓我們想想,,當(dāng)我們將一個(gè)Derived1型的對(duì)象傳給一個(gè)接受非引用Base型參數(shù)的函數(shù)時(shí)會(huì)發(fā)生切割,,那么是怎么切割的呢,?相信現(xiàn)在你已經(jīng)知道了,那就是僅僅取出了Derived1型的對(duì)象中的subobject,,而忽略了所有Derived1自定義的其它數(shù)據(jù)成員,,然后將這個(gè)subobject傳遞給函數(shù)(實(shí)際上,函數(shù)中使用的是這個(gè)subobject的拷貝),。
所有繼承Base類的派生類的對(duì)象都含有一個(gè)Base型的subobject(這是能用Base型指針指向一個(gè)Derived1對(duì)象的關(guān)鍵所在,,自然也是多態(tài)的關(guān)鍵了),而所有的subobject和所有Base型的對(duì)象都共用同一個(gè)s_object對(duì)象,,自然,,從Base類派生的整個(gè)繼承體系中的類的實(shí)例都會(huì)共用同一個(gè)s_object對(duì)象了。上面提到的example,、example1,、example2的對(duì)象布局如下圖所示:
3.1.2 三種內(nèi)存對(duì)象的比較
棧對(duì)象的優(yōu)勢(shì)是在適當(dāng)?shù)臅r(shí)候自動(dòng)生成,又在適當(dāng)?shù)臅r(shí)候自動(dòng)銷毀,,不需要程序員操心,;而且棧對(duì)象的創(chuàng)建速度一般較堆對(duì)象快,因?yàn)榉峙涠褜?duì)象時(shí),,會(huì)調(diào)用operator new操作,,operator new會(huì)采用某種內(nèi)存空間搜索算法,而該搜索過程可能是很費(fèi)時(shí)間的,,產(chǎn)生棧對(duì)象則沒有這么麻煩,,它僅僅需要移動(dòng)棧頂指針就可以了。但是要注意的是,,通常??臻g容量比較小,一般是1MB~2MB,,所以體積比較大的對(duì)象不適合在棧中分配,。特別要注意遞歸函數(shù)中最好不要使用棧對(duì)象,因?yàn)殡S著遞歸調(diào)用深度的增加,,所需的??臻g也會(huì)線性增加,當(dāng)所需??臻g不夠時(shí),,便會(huì)導(dǎo)致棧溢出,這樣就會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤,。
堆對(duì)象,其產(chǎn)生時(shí)刻和銷毀時(shí)刻都要程序員精確定義,,也就是說,,程序員對(duì)堆對(duì)象的生命具有完全的控制權(quán),。我們常常需要這樣的對(duì)象,比如,,我們需要?jiǎng)?chuàng)建一個(gè)對(duì)象,,能夠被多個(gè)函數(shù)所訪問,但是又不想使其成為全局的,,那么這個(gè)時(shí)候創(chuàng)建一個(gè)堆對(duì)象無疑是良好的選擇,,然后在各個(gè)函數(shù)之間傳遞這個(gè)堆對(duì)象的指針,便可以實(shí)現(xiàn)對(duì)該對(duì)象的共享,。另外,,相比于棧空間,,堆的容量要大得多,。實(shí)際上,當(dāng)物理內(nèi)存不夠時(shí),,如果這時(shí)還需要生成新的堆對(duì)象,,通常不會(huì)產(chǎn)生運(yùn)行時(shí)錯(cuò)誤,而是系統(tǒng)會(huì)使用虛擬內(nèi)存來擴(kuò)展實(shí)際的物理內(nèi)存,。
接下來看看static對(duì)象,。
首先是全局對(duì)象。全局對(duì)象為類間通信和函數(shù)間通信提供了一種最簡單的方式,,雖然這種方式并不優(yōu)雅,。一般而言,在完全的面向?qū)ο笳Z言中,,是不存在全局對(duì)象的,,比如C#,因?yàn)槿謱?duì)象意味著不安全和高耦合,,在程序中過多地使用全局對(duì)象將大大降低程序的健壯性,、穩(wěn)定性、可維護(hù)性和可復(fù)用性,。C++也完全可以剔除全局對(duì)象,,但是最終沒有,我想原因之一是為了兼容C,。
其次是類的靜態(tài)成員,,上面已經(jīng)提到,基類及其派生類的所有對(duì)象都共享這個(gè)靜態(tài)成員對(duì)象,,所以當(dāng)需要在這些class之間或這些class objects之間進(jìn)行數(shù)據(jù)共享或通信時(shí),,這樣的靜態(tài)成員無疑是很好的選擇。
接著是靜態(tài)局部對(duì)象,主要可用于保存該對(duì)象所在函數(shù)被屢次調(diào)用期間的中間狀態(tài),,其中一個(gè)最顯著的例子就是遞歸函數(shù),,我們都知道遞歸函數(shù)是自己調(diào)用自己的函數(shù),如果在遞歸函數(shù)中定義一個(gè)nonstatic局部對(duì)象,,那么當(dāng)遞歸次數(shù)相當(dāng)大時(shí),,所產(chǎn)生的開銷也是巨大的。這是因?yàn)閚onstatic局部對(duì)象是棧對(duì)象,,每遞歸調(diào)用一次,,就會(huì)產(chǎn)生一個(gè)這樣的對(duì)象,每返回一次,,就會(huì)釋放這個(gè)對(duì)象,,而且,這樣的對(duì)象只局限于當(dāng)前調(diào)用層,,對(duì)于更深入的嵌套層和更淺露的外層,,都是不可見的。每個(gè)層都有自己的局部對(duì)象和參數(shù),。
在遞歸函數(shù)設(shè)計(jì)中,,可以使用static對(duì)象替代nonstatic局部對(duì)象(即棧對(duì)象),這不僅可以減少每次遞歸調(diào)用和返回時(shí)產(chǎn)生和釋放nonstatic對(duì)象的開銷,,而且static對(duì)象還可以保存遞歸調(diào)用的中間狀態(tài),,并且可為各個(gè)調(diào)用層所訪問。
3.1.3 使用棧對(duì)象的意外收獲
前面已經(jīng)介紹到,,棧對(duì)象是在適當(dāng)?shù)臅r(shí)候創(chuàng)建,,然后在適當(dāng)?shù)臅r(shí)候自動(dòng)釋放的,也就是棧對(duì)象有自動(dòng)管理功能,。那么棧對(duì)象會(huì)在什么會(huì)自動(dòng)釋放了,?第一,在其生命期結(jié)束的時(shí)候,;第二,,在其所在的函數(shù)發(fā)生異常的時(shí)候。你也許說,,這些都很正常啊,,沒什么大不了的。是的,,沒什么大不了的,。但是只要我們?cè)偕钊胍稽c(diǎn)點(diǎn),也許就有意外的收獲了,。
棧對(duì)象,,自動(dòng)釋放時(shí),,會(huì)調(diào)用它自己的析構(gòu)函數(shù)。如果我們?cè)跅?duì)象中封裝資源,,而且在棧對(duì)象的析構(gòu)函數(shù)中執(zhí)行釋放資源的動(dòng)作,,那么就會(huì)使資源泄漏的概率大大降低,因?yàn)闂?duì)象可以自動(dòng)的釋放資源,,即使在所在函數(shù)發(fā)生異常的時(shí)候。實(shí)際的過程是這樣的:函數(shù)拋出異常時(shí),,會(huì)發(fā)生所謂的stack_unwinding(堆?;貪L),即堆棧會(huì)展開,,由于是棧對(duì)象,,自然存在于棧中,所以在堆?;貪L的過程中,,棧對(duì)象的析構(gòu)函數(shù)會(huì)被執(zhí)行,從而釋放其所封裝的資源,。除非,,除非在析構(gòu)函數(shù)執(zhí)行的過程中再次拋出異常――而這種可能性是很小的,所以用棧對(duì)象封裝資源是比較安全的,?;诖苏J(rèn)識(shí),我們就可以創(chuàng)建一個(gè)自己的句柄或代理來封裝資源了,。智能指針(auto_ptr)中就使用了這種技術(shù),。在有這種需要的時(shí)候,我們就希望我們的資源封裝類只能在棧中創(chuàng)建,,也就是要限制在堆中創(chuàng)建該資源封裝類的實(shí)例,。
3.1.4 禁止產(chǎn)生堆對(duì)象
上面已經(jīng)提到,你決定禁止產(chǎn)生某種類型的堆對(duì)象,,這時(shí)你可以自己創(chuàng)建一個(gè)資源封裝類,,該類對(duì)象只能在棧中產(chǎn)生,這樣就能在異常的情況下自動(dòng)釋放封裝的資源,。
那么怎樣禁止產(chǎn)生堆對(duì)象了,?我們已經(jīng)知道,產(chǎn)生堆對(duì)象的唯一方法是使用new操作,,如果我們禁止使用new不就行了么,。再進(jìn)一步,new操作執(zhí)行時(shí)會(huì)調(diào)用operator new,,而operator new是可以重載的,。方法有了,,就是使new operator 為private,為了對(duì)稱,,最好將operator delete也重載為private?,F(xiàn)在,你也許又有疑問了,難道創(chuàng)建棧對(duì)象不需要調(diào)用new嗎,?是的,,不需要,因?yàn)閯?chuàng)建棧對(duì)象不需要搜索內(nèi)存,,而是直接調(diào)整堆棧指針,,將對(duì)象壓棧,而operator new的主要任務(wù)是搜索合適的堆內(nèi)存,,為堆對(duì)象分配空間,,這在上面已經(jīng)提到過了。好,,讓我們看看下面的示例代碼:
#include <stdlib.h> //需要用到C式內(nèi)存分配函數(shù)
class Resource ; //代表需要被封裝的資源類
class NoHashObject
{
private:
Resource* ptr ;//指向被封裝的資源
... ... //其它數(shù)據(jù)成員
void* operator new(size_t size) //非嚴(yán)格實(shí)現(xiàn),,僅作示意之用
{
return malloc(size) ;
}
void operator delete(void* pp) //非嚴(yán)格實(shí)現(xiàn),僅作示意之用
{
free(pp) ;
}
public:
NoHashObject()
{
//此處可以獲得需要封裝的資源,,并讓ptr指針指向該資源
ptr = new Resource() ;
}
~NoHashObject()
{
delete ptr ; //釋放封裝的資源
}
};
NoHashObject現(xiàn)在就是一個(gè)禁止堆對(duì)象的類了,,如果你寫下如下代碼:
NoHashObject* fp = new NoHashObject() ; //編譯期錯(cuò)誤!
delete fp ;
上面代碼會(huì)產(chǎn)生編譯期錯(cuò)誤,。好了,,現(xiàn)在你已經(jīng)知道了如何設(shè)計(jì)一個(gè)禁止堆對(duì)象的類了,你也許和我一樣有這樣的疑問,,難道在類NoHashObject的定義不能改變的情況下,,就一定不能產(chǎn)生該類型的堆對(duì)象了嗎?不,,還是有辦法的,,我稱之為“暴力破解法”。C++是如此地強(qiáng)大,,強(qiáng)大到你可以用它做你想做的任何事情,。這里主要用到的是技巧是指針類型的強(qiáng)制轉(zhuǎn)換。
void main(void)
{
char* temp = new char[sizeof(NoHashObject)] ;
//強(qiáng)制類型轉(zhuǎn)換,,現(xiàn)在ptr是一個(gè)指向NoHashObject對(duì)象的指針
NoHashObject* obj_ptr = (NoHashObject*)temp ;
temp = NULL ; //防止通過temp指針修改NoHashObject對(duì)象
//再一次強(qiáng)制類型轉(zhuǎn)換,,讓rp指針指向堆中NoHashObject對(duì)象的ptr成員
Resource* rp = (Resource*)obj_ptr ;
//初始化obj_ptr指向的NoHashObject對(duì)象的ptr成員
rp = new Resource() ;
//現(xiàn)在可以通過使用obj_ptr指針使用堆中的NoHashObject對(duì)象成員了
... ...
delete rp ;//釋放資源
temp = (char*)obj_ptr ;
obj_ptr = NULL ;//防止懸掛指針產(chǎn)生
delete [] temp ;//釋放NoHashObject對(duì)象所占的堆空間。
}
上面的實(shí)現(xiàn)是麻煩的,,而且這種實(shí)現(xiàn)方式幾乎不會(huì)在實(shí)踐中使用,,但是我還是寫出來路,因?yàn)槔斫馑?,?duì)于我們理解C++內(nèi)存對(duì)象是有好處的,。對(duì)于上面的這么多強(qiáng)制類型轉(zhuǎn)換,,其最根本的是什么了?我們可以這樣理解:
某塊內(nèi)存中的數(shù)據(jù)是不變的,,而類型就是我們戴上的眼鏡,,當(dāng)我們戴上一種眼鏡后,我們就會(huì)用對(duì)應(yīng)的類型來解釋內(nèi)存中的數(shù)據(jù),,這樣不同的解釋就得到了不同的信息,。
所謂強(qiáng)制類型轉(zhuǎn)換實(shí)際上就是換上另一副眼鏡后再來看同樣的那塊內(nèi)存數(shù)據(jù)。
另外要提醒的是,,不同的編譯器對(duì)對(duì)象的成員數(shù)據(jù)的布局安排可能是不一樣的,,比如,大多數(shù)編譯器將NoHashObject的ptr指針成員安排在對(duì)象空間的頭4個(gè)字節(jié),,這樣才會(huì)保證下面這條語句的轉(zhuǎn)換動(dòng)作像我們預(yù)期的那樣執(zhí)行:
Resource* rp = (Resource*)obj_ptr ;
但是,并不一定所有的編譯器都是如此,。
既然我們可以禁止產(chǎn)生某種類型的堆對(duì)象,,那么可以設(shè)計(jì)一個(gè)類,使之不能產(chǎn)生棧對(duì)象嗎,?當(dāng)然可以,。
3.1.5 禁止產(chǎn)生棧對(duì)象
前面已經(jīng)提到了,創(chuàng)建棧對(duì)象時(shí)會(huì)移動(dòng)棧頂指針以“挪出”適當(dāng)大小的空間,,然后在這個(gè)空間上直接調(diào)用對(duì)應(yīng)的構(gòu)造函數(shù)以形成一個(gè)棧對(duì)象,,而當(dāng)函數(shù)返回時(shí),會(huì)調(diào)用其析構(gòu)函數(shù)釋放這個(gè)對(duì)象,,然后再調(diào)整棧頂指針收回那塊棧內(nèi)存,。在這個(gè)過程中是不需要operator new/delete操作的,所以將operator new/delete設(shè)置為private不能達(dá)到目的,。當(dāng)然從上面的敘述中,,你也許已經(jīng)想到了:將構(gòu)造函數(shù)或析構(gòu)函數(shù)設(shè)為私有的,這樣系統(tǒng)就不能調(diào)用構(gòu)造/析構(gòu)函數(shù)了,,當(dāng)然就不能在棧中生成對(duì)象了,。
這樣的確可以,而且我也打算采用這種方案,。但是在此之前,,有一點(diǎn)需要考慮清楚,那就是,如果我們將構(gòu)造函數(shù)設(shè)置為私有,,那么我們也就不能用new來直接產(chǎn)生堆對(duì)象了,,因?yàn)閚ew在為對(duì)象分配空間后也會(huì)調(diào)用它的構(gòu)造函數(shù)啊。所以,,我打算只將析構(gòu)函數(shù)設(shè)置為private,。再進(jìn)一步,,將析構(gòu)函數(shù)設(shè)為private除了會(huì)限制棧對(duì)象生成外,還有其它影響嗎,?是的,,這還會(huì)限制繼承。
如果一個(gè)類不打算作為基類,,通常采用的方案就是將其析構(gòu)函數(shù)聲明為private,。
為了限制棧對(duì)象,卻不限制繼承,,我們可以將析構(gòu)函數(shù)聲明為protected,,這樣就兩全其美了。如下代碼所示:
class NoStackObject
{
protected:
~NoStackObject() { }
public:
void destroy()
{
delete this ;//調(diào)用保護(hù)析構(gòu)函數(shù)
}
};
接著,,可以像這樣使用NoStackObject類:
NoStackObject* hash_ptr = new NoStackObject() ;
... ... //對(duì)hash_ptr指向的對(duì)象進(jìn)行操作
hash_ptr->destroy() ;
呵呵,,是不是覺得有點(diǎn)怪怪的,我們用new創(chuàng)建一個(gè)對(duì)象,,卻不是用delete去刪除它,,而是要用destroy方法。很顯然,,用戶是不習(xí)慣這種怪異的使用方式的,。所以,我決定將構(gòu)造函數(shù)也設(shè)為private或protected,。這又回到了上面曾試圖避免的問題,,即不用new,那么該用什么方式來生成一個(gè)對(duì)象了,?我們可以用間接的辦法完成,,即讓這個(gè)類提供一個(gè)static成員函數(shù)專門用于產(chǎn)生該類型的堆對(duì)象。(設(shè)計(jì)模式中的singleton模式就可以用這種方式實(shí)現(xiàn),。)讓我們來看看:
class NoStackObject
{
protected:
NoStackObject() { }
~NoStackObject() { }
public:
static NoStackObject* creatInstance()
{
return new NoStackObject() ;//調(diào)用保護(hù)的構(gòu)造函數(shù)
}
void destroy()
{
delete this ;//調(diào)用保護(hù)的析構(gòu)函數(shù)
}
};
現(xiàn)在可以這樣使用NoStackObject類了:
NoStackObject* hash_ptr = NoStackObject::creatInstance() ;
... ... //對(duì)hash_ptr指向的對(duì)象進(jìn)行操作
hash_ptr->destroy() ;
hash_ptr = NULL ; //防止使用懸掛指針
現(xiàn)在感覺是不是好多了,,生成對(duì)象和釋放對(duì)象的操作一致了。
3.2 淺議C++ 中的垃圾回收方法
許多 C 或者 C++ 程序員對(duì)垃圾回收嗤之以鼻,,認(rèn)為垃圾回收肯定比自己來管理動(dòng)態(tài)內(nèi)存要低效,,而且在回收的時(shí)候一定會(huì)讓程序停頓在那里,而如果自己控制內(nèi)存管理的話,,分配和釋放時(shí)間都是穩(wěn)定的,,不會(huì)導(dǎo)致程序停頓。最后,,很多 C/C++ 程序員堅(jiān)信在C/C++ 中無法實(shí)現(xiàn)垃圾回收機(jī)制,。這些錯(cuò)誤的觀點(diǎn)都是由于不了解垃圾回收的算法而臆想出來的。
其實(shí)垃圾回收機(jī)制并不慢,,甚至比動(dòng)態(tài)內(nèi)存分配更高效,。因?yàn)槲覀兛梢灾环峙洳会尫?,那么分配?nèi)存的時(shí)候只需要從堆上一直的獲得新的內(nèi)存,移動(dòng)堆頂?shù)闹羔樉蛪蛄?;而釋放的過程被省略了,,自然也加快了速度。現(xiàn)代的垃圾回收算法已經(jīng)發(fā)展了很多,,增量收集算法已經(jīng)可以讓垃圾回收過程分段進(jìn)行,,避免打斷程序的運(yùn)行了。而傳統(tǒng)的動(dòng)態(tài)內(nèi)存管理的算法同樣有在適當(dāng)?shù)臅r(shí)間收集內(nèi)存碎片的工作要做,,并不比垃圾回收更有優(yōu)勢(shì),。
而垃圾回收的算法的基礎(chǔ)通常基于掃描并標(biāo)記當(dāng)前可能被使用的所有內(nèi)存塊,,從已經(jīng)被分配的所有內(nèi)存中把未標(biāo)記的內(nèi)存回收來做的,。C/C++ 中無法實(shí)現(xiàn)垃圾回收的觀點(diǎn)通常基于無法正確掃描出所有可能還會(huì)被使用的內(nèi)存塊,,但是,,看似不可能的事情實(shí)際上實(shí)現(xiàn)起來卻并不復(fù)雜。首先,,通過掃描內(nèi)存的數(shù)據(jù),指向堆上動(dòng)態(tài)分配出來內(nèi)存的指針是很容易被識(shí)別出來的,,如果有識(shí)別錯(cuò)誤,,也只能是把一些不是指針的數(shù)據(jù)當(dāng)成指針,而不會(huì)把指針當(dāng)成非指針數(shù)據(jù),。這樣,,回收垃圾的過程只會(huì)漏回收掉而不會(huì)錯(cuò)誤的把不應(yīng)該回收的內(nèi)存清理。其次,,如果回溯所有內(nèi)存塊被引用的根,,只可能存在于全局變量和當(dāng)前的棧內(nèi),而全局變量(包括函數(shù)內(nèi)的靜態(tài)變量)都是集中存在于 bss 段或 data段中,。
垃圾回收的時(shí)候,,只需要掃描 bss 段, data 段以及當(dāng)前被使用著的棧空間,,找到可能是動(dòng)態(tài)內(nèi)存指針的量,,把引用到的內(nèi)存遞歸掃描就可以得到當(dāng)前正在使用的所有動(dòng)態(tài)內(nèi)存了。
如果肯為你的工程實(shí)現(xiàn)一個(gè)不錯(cuò)的垃圾回收器,,提高內(nèi)存管理的速度,,甚至減少總的內(nèi)存消耗都是可能的。如果有興趣的話,,可以搜索一下網(wǎng)上已有的關(guān)于垃圾回收的論文和實(shí)現(xiàn)了的庫,,開拓視野對(duì)一個(gè)程序員尤為重要,。