關(guān)于C++為什么不加入垃圾回收機(jī)制解析
Java的愛(ài)好者們經(jīng)常批評(píng)C++中沒(méi)有提供與Java類(lèi)似的垃圾回收(Gabage Collector)機(jī)制(這很正常,正如C++的愛(ài)好者有時(shí)也攻擊Java沒(méi)有這個(gè)沒(méi)有那個(gè),或者這個(gè)不行那個(gè)不夠好),導(dǎo)致C++中對(duì)動(dòng)態(tài)存儲(chǔ)的官吏稱(chēng)為程序員的噩夢(mèng),不是嗎?你經(jīng)常聽(tīng)到的是內(nèi)存遺失(memory leak)和非法指針存取,這一定令你很頭疼,而且你又不能拋棄指針帶來(lái)的靈活性。
在本文中,我并不想揭露Java提供的垃圾回收機(jī)制的天生缺陷,而是指出了C++中引入垃圾回收的可行性。請(qǐng)讀者注意,這里介紹的方法更多的是基于當(dāng)前標(biāo)準(zhǔn)和庫(kù)設(shè)計(jì)的角度,而不是要求修改語(yǔ)言定義或者擴(kuò)展編譯器。
什么是垃圾回收?
作為支持指針的編程語(yǔ)言,C++將動(dòng)態(tài)管理存儲(chǔ)器資源的便利性交給了程序員。在使用指針形式的對(duì)象時(shí)(請(qǐng)注意,由于引用在初始化后不能更改引用目標(biāo)的語(yǔ)言機(jī)制的限制,多態(tài)性應(yīng)用大多數(shù)情況下依賴(lài)于指針進(jìn)行),程序員必須自己完成存儲(chǔ)器的分配、使用和釋放,語(yǔ)言本身在此過(guò)程中不能提供任何幫助,也許除了按照你的要求正確的和操作系統(tǒng)親密合作,完成實(shí)際的存儲(chǔ)器管理。標(biāo)準(zhǔn)文本中,多次提到了“未定義(undefined)”,而這大多數(shù)情況下和指針相關(guān)。
某些語(yǔ)言提供了垃圾回收機(jī)制,也就是說(shuō)程序員僅負(fù)責(zé)分配存儲(chǔ)器和使用,而由語(yǔ)言本身負(fù)責(zé)釋放不再使用的存儲(chǔ)器,這樣程序員就從討厭的存儲(chǔ)器管理的工作中脫身了。然而C++并沒(méi)有提供類(lèi)似的機(jī)制,C++的設(shè)計(jì)者Bjarne Stroustrup在我所知的唯一一本介紹語(yǔ)言設(shè)計(jì)的思想和哲學(xué)的著作《The Design and Evolution of C++》(中譯本:C++語(yǔ)言的設(shè)計(jì)和演化)中花了一個(gè)小節(jié)討論這個(gè)特性。簡(jiǎn)而言之,Bjarne本人認(rèn)為,
“我有意這樣設(shè)計(jì)C++,使它不依賴(lài)于自動(dòng)垃圾回收(通常就直接說(shuō)垃圾回收)。這是基于自己對(duì)垃圾回收系統(tǒng)的經(jīng)驗(yàn),我很害怕那種嚴(yán)重的空間和時(shí)間開(kāi)銷(xiāo),也害怕由于實(shí)現(xiàn)和移植垃圾回收系統(tǒng)而帶來(lái)的復(fù)雜性。還有,垃圾回收將使C++不適合做許多底層的工作,而這卻正是它的一個(gè)設(shè)計(jì)目標(biāo)。但我喜歡垃圾回收的思想,它是一種機(jī)制,能夠簡(jiǎn)化設(shè)計(jì)、排除掉許多產(chǎn)生錯(cuò)誤的根源。
需要垃圾回收的基本理由是很容易理解的:用戶(hù)的使用方便以及比用戶(hù)提供的存儲(chǔ)管理模式更可靠。而反對(duì)垃圾回收的理由也有很多,但都不是最根本的,而是關(guān)于實(shí)現(xiàn)和效率方面的。
已經(jīng)有充分多的論據(jù)可以反駁:每個(gè)應(yīng)用在有了垃圾回收之后會(huì)做的更好些。類(lèi)似的,也有充分的論據(jù)可以反對(duì):沒(méi)有應(yīng)用可能因?yàn)橛辛死厥斩龅酶谩?/p>
并不是每個(gè)程序都需要永遠(yuǎn)無(wú)休止的運(yùn)行下去;并不是所有的代碼都是基礎(chǔ)性的庫(kù)代碼;對(duì)于許多應(yīng)用而言,出現(xiàn)一點(diǎn)存儲(chǔ)流失是可以接受的;許多應(yīng)用可以管理自己的存儲(chǔ),而不需要垃圾回收或者其他與之相關(guān)的技術(shù),如引用計(jì)數(shù)等。
我的結(jié)論是,從原則上和可行性上說(shuō),垃圾回收都是需要的。但是對(duì)今天的用戶(hù)以及普遍的使用和硬件而言,我們還無(wú)法承受將C++的語(yǔ)義和它的基本庫(kù)定義在垃圾回收系統(tǒng)之上的負(fù)擔(dān)?!?/p>
以我之見(jiàn),統(tǒng)一的自動(dòng)垃圾回收系統(tǒng)無(wú)法適用于各種不同的應(yīng)用環(huán)境,而又不至于導(dǎo)致實(shí)現(xiàn)上的負(fù)擔(dān)。稍后我將設(shè)計(jì)一個(gè)針對(duì)特定類(lèi)型的可選的垃圾回收器,可以很明顯地看到,或多或少總是存在一些效率上的開(kāi)銷(xiāo),如果強(qiáng)迫C++用戶(hù)必須接受這一點(diǎn),也許是不可取的。
關(guān)于為什么C++沒(méi)有垃圾回收以及可能的在C++中為此做出的努力,上面提到的著作是我所看過(guò)的對(duì)這個(gè)問(wèn)題敘述的最全面的,盡管只有短短的一個(gè)小節(jié)的內(nèi)容,但是已經(jīng)涵蓋了很多內(nèi)容,這正是Bjarne著作的一貫特點(diǎn),言簡(jiǎ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ù)中申請(qǐng)完成,而在析構(gòu)函數(shù)中釋放已經(jīng)分配的資源,只要對(duì)象的生存期結(jié)束,對(duì)象請(qǐng)求分配的資源即被自動(dòng)釋放。
那么就僅剩下一個(gè)問(wèn)題了,如果對(duì)象本身是在自由存儲(chǔ)區(qū)(Free Store,也就是所謂的“堆”)中動(dòng)態(tài)創(chuàng)建的,并由指針管理(相信你已經(jīng)知道為什么了),則還是必須通過(guò)編碼顯式的調(diào)用析構(gòu)函數(shù),當(dāng)然是借助指針的delete表達(dá)式。
智能指針
幸運(yùn)的是,出于某些原因,C++的標(biāo)準(zhǔn)庫(kù)中至少引入了一種類(lèi)型的智能指針,雖然在使用上有局限性,但是它剛好可以解決我們的這個(gè)難題,這就是標(biāo)準(zhǔn)庫(kù)中唯一的一個(gè)智能指針::std::auto_ptr<>。
它將指針包裝成了類(lèi),并且重載了反引用(dereference)運(yùn)算符operator *和成員選擇運(yùn)算符operator ->,以模仿指針的行為。關(guān)于auto_ptr<>的具體細(xì)節(jié),參閱《The C++ Standard Library》(中譯本:C++標(biāo)準(zhǔn)庫(kù))。
例如以下代碼,
#include < cstring > #include < memory > #include < iostream > 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ù)的局部對(duì)象,因此在函數(shù)退出點(diǎn)生存期結(jié)束,此時(shí)auto_ptr<string>的析構(gòu)函數(shù)調(diào)用,自動(dòng)銷(xiāo)毀內(nèi)部指針維護(hù)的string對(duì)象(先前在構(gòu)造函數(shù)中通過(guò)new表達(dá)式分配而來(lái)的),并進(jìn)而執(zhí)行string的析構(gòu)函數(shù),釋放為實(shí)際的字符串動(dòng)態(tài)申請(qǐng)的內(nèi)存。在string中也可能管理其他類(lèi)型的資源,如用于多線(xiàn)程環(huán)境下的同步資源。下圖說(shuō)明了上面的過(guò)程。
進(jìn)入函數(shù)foo 退出函數(shù)
| A
V |
auto_ptr<string>::auto<string>() auto_ptr<string>::~auto_ptr<string>()
| A
V |
string::string() string::~string()
| A
V |
_data=new char[] delete [] _data
| A
V |
使用資源 -----------------------------------> 釋放資源
現(xiàn)在我們擁有了最簡(jiǎn)單的垃圾回收機(jī)制(我隱瞞了一點(diǎn),在string中,你仍然需要自己編碼控制對(duì)象的動(dòng)態(tài)創(chuàng)建和銷(xiāo)毀,但是這種情況下的準(zhǔn)則極其簡(jiǎ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)移的語(yǔ)義,即智能指針在復(fù)制時(shí)將對(duì)內(nèi)部維護(hù)的實(shí)際指針的所有權(quán)進(jìn)行了轉(zhuǎn)移,例如
auto_ptr < string > str1( new string( < str1 > ) );
cout << str1->c_str();
auto_ptr < string > str2(str1); // str1內(nèi)部指針不再指向原來(lái)的對(duì)象
cout << str2->c_str();
cout << str1->c_str(); // 未定義,str1內(nèi)部指針不再有效
某些時(shí)候,需要共享同一個(gè)對(duì)象,此時(shí)auto_ptr就不敷使用,由于某些歷史的原因,C++的標(biāo)準(zhǔn)庫(kù)中并沒(méi)有提供其他形式的智能指針,走投無(wú)路了嗎?
另一種智能指針
但是我們可以自己制作另一種形式的智能指針,也就是具有值復(fù)制語(yǔ)義的,并且共享值的智能指針。
需要同一個(gè)類(lèi)的多個(gè)對(duì)象同時(shí)擁有一個(gè)對(duì)象的拷貝時(shí),我們可以使用引用計(jì)數(shù)(Reference Counting/Using Counting)來(lái)實(shí)現(xiàn),曾經(jīng)這是一個(gè)C++中為了提高效率與COW(copy on write,改寫(xiě)時(shí)復(fù)制)技術(shù)一起被廣泛使用的技術(shù),后來(lái)證明在多線(xiàn)程應(yīng)用中,COW為了保證行為的正確反而導(dǎo)致了效率降低(Herb Shutter的在C++ Report雜志中的Guru專(zhuān)欄以及整理后出版的《More Exceptional C++》中專(zhuān)門(mén)討論了這個(gè)問(wèn)題)。
然而對(duì)于我們目前的問(wèn)題,引用計(jì)數(shù)本身并不會(huì)有太大的問(wèn)題,因?yàn)闆](méi)有牽涉到復(fù)制問(wèn)題,為了保證多線(xiàn)程環(huán)境下的正確,并不需要過(guò)多的效率犧牲,但是為了簡(jiǎn)化問(wèn)題,這里忽略了對(duì)于多線(xiàn)程安全的考慮。
首先我們仿造auto_ptr設(shè)計(jì)了一個(gè)類(lèi)模板(出自Herb Shutter的《More Execptional C++》),
template < typename T > class shared_ptr { private: class implement // 實(shí)現(xiàn)類(lèi),引用計(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& rhs) : _impl(rhs._impl) { increase(); // 計(jì)數(shù)遞增 } shared_ptr& operator=(const shared_ptr& rhs) { if (_impl != rhs._impl) // 避免自賦值 { decrease(); // 計(jì)數(shù)遞減,不再共享原對(duì)象 _impl=rhs._impl; // 共享新的對(duì)象 increase(); // 計(jì)數(shù)遞增,維護(hù)正確的引用計(jì)數(shù)值 } return *this; } T* operator->() const { return _impl->p; } T& operator*() const { return *(_impl->p); } private: void decrease() { if (--(_impl->refs)==0) { // 不再被共享,銷(xiāo)毀對(duì)象 delete _impl; } } void increase() { ++(_impl->refs); } };
這個(gè)類(lèi)模板是如此的簡(jiǎn)單,所以都不需要對(duì)代碼進(jìn)行太多地說(shuō)明。這里僅僅給出一個(gè)簡(jiǎn)單的使用實(shí)例,足以說(shuō)明shared_ptr<>作為簡(jiǎn)單的垃圾回收器的替代品。
void foo1(shared_ptr < int >& val) { shared_ptr < int > temp(val); *temp=300; } void foo2(shared_ptr < int >& val) { val=shared_ptr < int > ( new int(200) ); } int main() { shared_ptr < int > val(new int(100)); cout<<"val="<<*val; foo1(val); cout<<"val="<<*val; foo2(val); cout<<"val="<<*val; }
在main()函數(shù)中,先調(diào)用foo1(val),函數(shù)中使用了一個(gè)局部對(duì)象temp,它和val共享同一份數(shù)據(jù),并修改了實(shí)際值,函數(shù)返回后,val擁有的值同樣也發(fā)生了變化,而實(shí)際上val本身并沒(méi)有修改過(guò)。
然后調(diào)用了foo2(val),函數(shù)中使用了一個(gè)無(wú)名的臨時(shí)對(duì)象創(chuàng)建了一個(gè)新值,使用賦值表達(dá)式修改了val,同時(shí)val和臨時(shí)對(duì)象擁有同一個(gè)值,函數(shù)返回時(shí),val仍然擁有這正確的值。
最后,在整個(gè)過(guò)程中,除了在使用shared_ptr < int >的構(gòu)造函數(shù)時(shí)使用了new表達(dá)式創(chuàng)建新之外,并沒(méi)有任何刪除指針的動(dòng)作,但是所有的內(nèi)存管理均正確無(wú)誤,這就是得益于shared_ptr<>的精巧的設(shè)計(jì)。
擁有了auto_ptr<>和shared_ptr<>兩大利器以后,應(yīng)該足以應(yīng)付大多數(shù)情況下的垃圾回收了,如果你需要更復(fù)雜語(yǔ)義(主要是指復(fù)制時(shí)的語(yǔ)義)的智能指針,可以參考boost的源代碼,其中設(shè)計(jì)了多種類(lèi)型的智能指針。
標(biāo)準(zhǔn)容器
對(duì)于需要在程序中擁有相同類(lèi)型的多個(gè)對(duì)象,善用標(biāo)準(zhǔn)庫(kù)提供的各種容器類(lèi),可以最大限度的杜絕顯式的內(nèi)存管理,然而標(biāo)準(zhǔn)容器并不適用于儲(chǔ)存指針,這樣對(duì)于多態(tài)性的支持仍然面臨困境。
使用智能指針作為容器的元素類(lèi)型,然而標(biāo)準(zhǔn)容器和算法大多數(shù)需要值復(fù)制語(yǔ)義的元素,前面介紹的轉(zhuǎn)移所有權(quán)的auto_ptr和自制的共享對(duì)象的shared_ptr都不能提供正確的值復(fù)制語(yǔ)義,Herb Sutter在《More Execptional C++》中設(shè)計(jì)了一個(gè)具有完全復(fù)制語(yǔ)義的智能指針ValuePtr,解決了指針用于標(biāo)準(zhǔn)容器的問(wèn)題。
然而,多態(tài)性仍然沒(méi)有解決,我將在另一篇文章專(zhuān)門(mén)介紹使用容器管理多態(tài)對(duì)象的問(wèn)題。
語(yǔ)言支持
為什么不在C++語(yǔ)言中增加對(duì)垃圾回收的支持?
根據(jù)前面的討論,我們可以看見(jiàn),不同的應(yīng)用環(huán)境,也許需要不同的垃圾回收器,不管三七二十一使用垃圾回收,需要將這些不同類(lèi)型的垃圾回收器整合在一起,即使可以成功(對(duì)此我感到懷疑),也會(huì)導(dǎo)致效率成本的增加。
這違反了C++的設(shè)計(jì)哲學(xué),“不為不必要的功能支付代價(jià)”,強(qiáng)迫用戶(hù)接受垃圾回收的代價(jià)并不可取。
相反,按需選擇你自己需要的垃圾回收器,需要掌握的規(guī)則與顯式的管理內(nèi)存相比,簡(jiǎn)單的多,也不容易出錯(cuò)。
最關(guān)鍵的一點(diǎn), C++并不是“傻瓜型”的編程語(yǔ)言,他青睞喜歡和善于思考的編程者,設(shè)計(jì)一個(gè)合適自己需要的垃圾回收器,正是對(duì)喜愛(ài)C++的程序員的一種挑戰(zhàn)。
以上就是小編為大家?guī)?lái)的關(guān)于C++為什么不加入垃圾回收機(jī)制解析全部?jī)?nèi)容了,希望大家多多支持腳本之家~
相關(guān)文章
使用VC6.0對(duì)C語(yǔ)言程序進(jìn)行調(diào)試的基本手段分享
這篇文章主要介紹了用VC6.0開(kāi)發(fā)c語(yǔ)言程序的時(shí)候調(diào)試代碼的一些小技巧,需要的朋友可以參考下2013-07-07一篇文章帶你用C語(yǔ)言玩轉(zhuǎn)結(jié)構(gòu)體
本文主要介紹C語(yǔ)言 結(jié)構(gòu)體的知識(shí),學(xué)習(xí)C語(yǔ)言肯定需要學(xué)習(xí)結(jié)構(gòu)體,這里詳細(xì)說(shuō)明了結(jié)構(gòu)體并附示例代碼,供大家參考學(xué)習(xí),有需要的小伙伴可以參考下2021-09-09C語(yǔ)言中深度優(yōu)先搜索(DFS)算法的示例詳解
這篇文章主要通過(guò)兩個(gè)簡(jiǎn)單的示例為大家詳細(xì)介紹一下C語(yǔ)言中深度優(yōu)先搜索(DFS)算法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-02-02C++ new/delete相關(guān)知識(shí)點(diǎn)詳細(xì)解析
C語(yǔ)言用一堆標(biāo)準(zhǔn)庫(kù)函數(shù)malloc和free在自由存儲(chǔ)區(qū)中分配存儲(chǔ)空間,而C++則用new和delete表達(dá)式實(shí)現(xiàn)相同的功能2013-09-09C語(yǔ)言數(shù)據(jù)在內(nèi)存中的存儲(chǔ)詳解
本篇文章是C語(yǔ)言編程篇,主要為大家介紹C語(yǔ)言編程中數(shù)據(jù)在內(nèi)存中存儲(chǔ)解析,有需要的朋友可以借鑒參考下,希望可以有所幫助2021-09-09C++簡(jiǎn)單又輕松建立鏈?zhǔn)蕉鏄?shù)流程
二叉樹(shù)的鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu)是指,用鏈表來(lái)表示一棵二叉樹(shù),即用鏈來(lái)指示元素的邏輯關(guān)系。通常的方法是鏈表中每個(gè)結(jié)點(diǎn)由三個(gè)域組成,數(shù)據(jù)域和左右指針域,左右指針?lè)謩e用來(lái)給出該結(jié)點(diǎn)左孩子和右孩子所在的鏈結(jié)點(diǎn)的存儲(chǔ)地址2022-06-06