C 為什么不加入垃圾回收機(jī)制
時(shí)間:2021-08-19 16:29:22
手機(jī)看文章
掃描二維碼
隨時(shí)隨地手機(jī)看文章
[導(dǎo)讀]來源:http://www.codeceo.com/article/why-cpp-not-use-gc.html作者:M-先生Java的愛好者們經(jīng)常批評(píng)C中沒有提供與Java類似的垃圾回收(GabageCollector)機(jī)制(這很正常,正如C的愛好者有時(shí)也攻擊Java沒有這個(gè)...
來源:http://www.codeceo.com/article/why-cpp-not-use-gc.html作者:M-先生
在本文中,我并不想揭露Java提供的垃圾回收機(jī)制的天生缺陷,而是指出了C 中引入垃圾回收的可行性。請讀者注意,這里介紹的方法更多的是基于當(dāng)前標(biāo)準(zhǔn)和庫設(shè)計(jì)的角度,而不是要求修改語言定義或者擴(kuò)展編譯器。
什么是垃圾回收?
作為支持指針的編程語言,C 將動(dòng)態(tài)管理存儲(chǔ)器資源的便利性交給了程序員。在使用指針形式的對象時(shí)(請注意,由于引用在初始化后不能更改引用目標(biāo)的語言機(jī)制的限制,多態(tài)性應(yīng)用大多數(shù)情況下依賴于指針進(jìn)行),程序員必須自己完成存儲(chǔ)器的分配、使用和釋放,語言本身在此過程中不能提供任何幫助,也許除了按照你的要求正確的和操作系統(tǒng)親密合作,完成實(shí)際的存儲(chǔ)器管理。標(biāo)準(zhǔn)文本中,多次提到了“未定義(undefined)”,而這大多數(shù)情況下和指針相關(guān)。某些語言提供了垃圾回收機(jī)制,也就是說程序員僅負(fù)責(zé)分配存儲(chǔ)器和使用,而由語言本身負(fù)責(zé)釋放不再使用的存儲(chǔ)器,這樣程序員就從討厭的存儲(chǔ)器管理的工作中脫身了。然而C 并沒有提供類似的機(jī)制,C 的設(shè)計(jì)者Bjarne Stroustrup在我所知的唯一一本介紹語言設(shè)計(jì)的思想和哲學(xué)的著作《The Design and Evolution of C 》(中譯本:C 語言的設(shè)計(jì)和演化)中花了一個(gè)小節(jié)討論這個(gè)特性。簡而言之,Bjarne本人認(rèn)為,“我有意這樣設(shè)計(jì)C ,使它不依賴于自動(dòng)垃圾回收(通常就直接說垃圾回收)。這是基于自己對垃圾回收系統(tǒng)的經(jīng)驗(yàn),我很害怕那種嚴(yán)重的空間和時(shí)間開銷,也害怕由于實(shí)現(xiàn)和移植垃圾回收系統(tǒng)而帶來的復(fù)雜性。還有,垃圾回收將使C 不適合做許多底層的工作,而這卻正是它的一個(gè)設(shè)計(jì)目標(biāo)。但我喜歡垃圾回收的思想,它是一種機(jī)制,能夠簡化設(shè)計(jì)、排除掉許多產(chǎn)生錯(cuò)誤的根源。需要垃圾回收的基本理由是很容易理解的:用戶的使用方便以及比用戶提供的存儲(chǔ)管理模式更可靠。而反對垃圾回收的理由也有很多,但都不是最根本的,而是關(guān)于實(shí)現(xiàn)和效率方面的。已經(jīng)有充分多的論據(jù)可以反駁:每個(gè)應(yīng)用在有了垃圾回收之后會(huì)做的更好些。類似的,也有充分的論據(jù)可以反對:沒有應(yīng)用可能因?yàn)橛辛死厥斩龅酶谩?/span>并不是每個(gè)程序都需要永遠(yuǎn)無休止的運(yùn)行下去;并不是所有的代碼都是基礎(chǔ)性的庫代碼;對于許多應(yīng)用而言,出現(xiàn)一點(diǎn)存儲(chǔ)流失是可以接受的;許多應(yīng)用可以管理自己的存儲(chǔ),而不需要垃圾回收或者其他與之相關(guān)的技術(shù),如引用計(jì)數(shù)等。我的結(jié)論是,從原則上和可行性上說,垃圾回收都是需要的。但是對今天的用戶以及普遍的使用和硬件而言,我們還無法承受將C 的語義和它的基本庫定義在垃圾回收系統(tǒng)之上的負(fù)擔(dān)。”以我之見,統(tǒng)一的自動(dòng)垃圾回收系統(tǒng)無法適用于各種不同的應(yīng)用環(huán)境,而又不至于導(dǎo)致實(shí)現(xiàn)上的負(fù)擔(dān)。稍后我將設(shè)計(jì)一個(gè)針對特定類型的可選的垃圾回收器,可以很明顯地看到,或多或少總是存在一些效率上的開銷,如果強(qiáng)迫C 用戶必須接受這一點(diǎn),也許是不可取的。關(guān)于為什么C 沒有垃圾回收以及可能的在C 中為此做出的努力,上面提到的著作是我所看過的對這個(gè)問題敘述的最全面的,盡管只有短短的一個(gè)小節(jié)的內(nèi)容,但是已經(jīng)涵蓋了很多內(nèi)容,這正是Bjarne著作的一貫特點(diǎn),言簡意賅而內(nèi)韻十足。下面一步一步地向大家介紹我自己土制佳釀的垃圾回收系統(tǒng),可以按照需要自由選用,而不影響其他代碼。構(gòu)造函數(shù)和析構(gòu)函數(shù)
C 中提供的構(gòu)造函數(shù)和析構(gòu)函數(shù)很好的解決了自動(dòng)釋放資源的需求。Bjarne有一句名言,“資源需求就是初始化(Resource Inquirment Is Initialization)”。因此,我們可以將需要分配的資源在構(gòu)造函數(shù)中申請完成,而在析構(gòu)函數(shù)中釋放已經(jīng)分配的資源,只要對象的生存期結(jié)束,對象請求分配的資源即被自動(dòng)釋放。那么就僅剩下一個(gè)問題了,如果對象本身是在自由存儲(chǔ)區(qū)(Free Store,也就是所謂的“堆”)中動(dòng)態(tài)創(chuàng)建的,并由指針管理(相信你已經(jīng)知道為什么了),則還是必須通過編碼顯式的調(diào)用析構(gòu)函數(shù),當(dāng)然是借助指針的delete表達(dá)式。智能指針
幸運(yùn)的是,出于某些原因,C 的標(biāo)準(zhǔn)庫中至少引入了一種類型的智能指針,雖然在使用上有局限性,但是它剛好可以解決我們的這個(gè)難題,這就是標(biāo)準(zhǔn)庫中唯一的一個(gè)智能指針::std::auto_ptr。它將指針包裝成了類,并且重載了反引用(dereference)運(yùn)算符operator *和成員選擇運(yùn)算符operator ->,以模仿指針的行為。關(guān)于auto_ptr的具體細(xì)節(jié),參閱《The C Standard Library》(中譯本:C 標(biāo)準(zhǔn)庫)。例如以下代碼,#include#include
#include
class string
{
public:
string(const char* cstr) { _data=new char [ strlen(cstr) 1 ]; strcpy(_data, cstr); }
~string() { delete [] _data; }
const char* c_str() const { return _data; }
private:
char* _data;
};
void foo()
{
::std::auto_ptr <string> str ( new string( " hello " ) );
::std::cout << str->c_str() << ::std::endl;
}由于str是函數(shù)的局部對象,因此在函數(shù)退出點(diǎn)生存期結(jié)束,此時(shí)auto_ptr的析構(gòu)函數(shù)調(diào)用,自動(dòng)銷毀內(nèi)部指針維護(hù)的string對象(先前在構(gòu)造函數(shù)中通過new表達(dá)式分配而來的),并進(jìn)而執(zhí)行string的析構(gòu)函數(shù),釋放為實(shí)際的字符串動(dòng)態(tài)申請的內(nèi)存。在string中也可能管理其他類型的資源,如用于多線程環(huán)境下的同步資源。下圖說明了上面的過程。現(xiàn)在我們擁有了最簡單的垃圾回收機(jī)制(我隱瞞了一點(diǎn),在string中,你仍然需要自己編碼控制對象的動(dòng)態(tài)創(chuàng)建和銷毀,但是這種情況下的準(zhǔn)則極其簡單,就是在構(gòu)造函數(shù)中分配資源,在析構(gòu)函數(shù)中釋放資源,就好像飛機(jī)駕駛員必須在起飛后和降落前檢查起落架一樣。),即使在foo函數(shù)中發(fā)生了異常,str的生存期也會(huì)結(jié)束,C 保證自然退出時(shí)發(fā)生的一切在異常發(fā)生時(shí)一樣會(huì)有效。auto_ptr只是智能指針的一種,它的復(fù)制行為提供了所有權(quán)轉(zhuǎn)移的語義,即智能指針在復(fù)制時(shí)將對內(nèi)部維護(hù)的實(shí)際指針的所有權(quán)進(jìn)行了轉(zhuǎn)移,例如auto_ptr <string> str1( new string(
cout << str1->c_str();
auto_ptr <string> str2(str1); // str1內(nèi)部指針不再指向原來的對象
cout << str2->c_str();
cout << str1->c_str(); // 未定義,str1內(nèi)部指針不再有效某些時(shí)候,需要共享同一個(gè)對象,此時(shí)auto_ptr就不敷使用,由于某些歷史的原因,C 的標(biāo)準(zhǔn)庫中并沒有提供其他形式的智能指針,走投無路了嗎?
另一種智能指針
但是我們可以自己制作另一種形式的智能指針,也就是具有值復(fù)制語義的,并且共享值的智能指針。需要同一個(gè)類的多個(gè)對象同時(shí)擁有一個(gè)對象的拷貝時(shí),我們可以使用引用計(jì)數(shù)(Reference Counting/Using Counting)來實(shí)現(xiàn),曾經(jīng)這是一個(gè)C 中為了提高效率與COW(copy on write,改寫時(shí)復(fù)制)技術(shù)一起被廣泛使用的技術(shù),后來證明在多線程應(yīng)用中,COW為了保證行為的正確反而導(dǎo)致了效率降低(Herb Shutter的在C Report雜志中的Guru專欄以及整理后出版的《More Exceptional C 》中專門討論了這個(gè)問題)。然而對于我們目前的問題,引用計(jì)數(shù)本身并不會(huì)有太大的問題,因?yàn)闆]有牽涉到復(fù)制問題,為了保證多線程環(huán)境下的正確,并不需要過多的效率犧牲,但是為了簡化問題,這里忽略了對于多線程安全的考慮。首先我們仿造auto_ptr設(shè)計(jì)了一個(gè)類模板(出自Herb Shutter的《More Execptional C 》),templateclass shared_ptr
{
private:
class implement // 實(shí)現(xiàn)類,引用計(jì)數(shù)
{
public:
implement(T* pp):p(pp),refs(1){}
~implement(){delete p;}
T* p; // 實(shí)際指針
size_t refs; // 引用計(jì)數(shù)
};
implement* _impl;
public:
explicit shared_ptr(T* p)
: _impl(new implement(p)){}
~shared_ptr()
{
decrease(); // 計(jì)數(shù)遞減
}
shared_ptr(const shared_ptr