C++ 為什麼不加入垃圾回收機制
Java的愛好者們經常批評C++中沒有提供與Java類似的垃圾回收(Gabage Collector)機制(這很正常,正如C++的愛好者有時也攻擊Java沒有這個沒有那個,或者這個不行那個不夠好),導致C++中對動態儲存的官吏稱為程式設計師的噩夢,不是嗎?你經常聽到的是記憶體遺失(memory leak)和非法指標存取,這一定令你很頭疼,而且你又不能拋棄指標帶來的靈活性。
在本文中,我並不想揭露Java提供的垃圾回收機制的天生缺陷,而是指出了C++中引入垃圾回收的可行性。請讀者注意,這裡介紹的方法更多的是基於當前標準和庫設計的角度,而不是要求修改語言定義或者擴充套件編譯器。
什麼是垃圾回收?
作為支援指標的程式語言,C++將動態管理儲存器資源的便利性交給了程式設計師。在使用指標形式的物件時(請注意,由於引用在初始化後不能更改引用目標的語言機制的限制,多型性應用大多數情況下依賴於指標進行),程式設計師必須自己完成儲存器的分配、使用和釋放,語言本身在此過程中不能提供任何幫助,也許除了按照你的要求正確的和作業系統親密合作,完成實際的儲存器管理。標準文字中,多次提到了“未定義(undefined)”,而這大多數情況下和指標相關。
某些語言提供了垃圾回收機制,也就是說程式設計師僅負責分配儲存器和使用,而由語言本身負責釋放不再使用的儲存器,這樣程式設計師就從討厭的儲存器管理的工作中脫身了。然而C++並沒有提供類似的機制,C++的設計者Bjarne Stroustrup在我所知的唯一一本介紹語言設計的思想和哲學的著作《The Design and Evolution of C++》(中譯本:C++語言的設計和演化)中花了一個小節討論這個特性。簡而言之,Bjarne本人認為,
“我有意這樣設計C++,使它不依賴於自動垃圾回收(通常就直接說垃圾回收)。這是基於自己對垃圾回收系統的經驗,我很害怕那種嚴重的空間和時間開銷,也害怕由於實現和移植垃圾回收系統而帶來的複雜性。還有,垃圾回收將使C++不適合做許多底層的工作,而這卻正是它的一個設計目標。但我喜歡垃圾回收的思想,它是一種機制,能夠簡化設計、排除掉許多產生錯誤的根源。
需要垃圾回收的基本理由是很容易理解的:使用者的使用方便以及比使用者提供的儲存管理模式更可靠。而反對垃圾回收的理由也有很多,但都不是最根本的,而是關於實現和效率方面的。
已經有充分多的論據可以反駁:每個應用在有了垃圾回收之後會做的更好些。類似的,也有充分的論據可以反對:沒有應用可能因為有了垃圾回收而做得更好。
並不是每個程式都需要永遠無休止的執行下去;並不是所有的程式碼都是基礎性的庫程式碼;對於許多應用而言,出現一點儲存流失是可以接受的;許多應用可以管理自己的儲存,而不需要垃圾回收或者其他與之相關的技術,如引用計數等。
我的結論是,從原則上和可行性上說,垃圾回收都是需要的。但是對今天的使用者以及普遍的使用和硬體而言,我們還無法承受將C++的語義和它的基本庫定義在垃圾回收系統之上的負擔。”
以我之見,統一的自動垃圾回收系統無法適用於各種不同的應用環境,而又不至於導致實現上的負擔。稍後我將設計一個針對特定型別的可選的垃圾回收器,可以很明顯地看到,或多或少總是存在一些效率上的開銷,如果強迫C++使用者必須接受這一點,也許是不可取的。
關於為什麼C++沒有垃圾回收以及可能的在C++中為此做出的努力,上面提到的著作是我所看過的對這個問題敘述的最全面的,儘管只有短短的一個小節的內容,但是已經涵蓋了很多內容,這正是Bjarne著作的一貫特點,言簡意賅而內韻十足。
下面一步一步地向大家介紹我自己土製佳釀的垃圾回收系統,可以按照需要自由選用,而不影響其他程式碼。
建構函式和解構函式
C++中提供的建構函式和解構函式很好的解決了自動釋放資源的需求。Bjarne有一句名言,“資源需求就是初始化(Resource Inquirment Is Initialization)”。
因此,我們可以將需要分配的資源在建構函式中申請完成,而在解構函式中釋放已經分配的資源,只要物件的生存期結束,物件請求分配的資源即被自動釋放。
那麼就僅剩下一個問題了,如果物件本身是在自由儲存區(Free Store,也就是所謂的“堆”)中動態建立的,並由指標管理(相信你已經知道為什麼了),則還是必須通過編碼顯式的呼叫解構函式,當然是藉助指標的delete表示式。
智慧指標
幸運的是,出於某些原因,C++的標準庫中至少引入了一種型別的智慧指標,雖然在使用上有侷限性,但是它剛好可以解決我們的這個難題,這就是標準庫中唯一的一個智慧指標::std::auto_ptr。
它將指標包裝成了類,並且過載了反引用(dereference)運算子operator *和成員選擇運算子operator ->,以模仿指標的行為。關於auto_ptr的具體細節,參閱《The C++ Standard Library》(中譯本:C++標準庫)。
例如以下程式碼,
#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是函式的區域性物件,因此在函式退出點生存期結束,此時auto_ptr的解構函式呼叫,自動銷燬內部指標維護的string物件(先前在建構函式中通過new表示式分配而來的),並進而執行string的解構函式,釋放為實際的字串動態申請的記憶體。在string中也可能管理其他型別的資源,如用於多執行緒環境下的同步資源。下圖說明了上面的過程。
現在我們擁有了最簡單的垃圾回收機制(我隱瞞了一點,在string中,你仍然需要自己編碼控制物件的動態建立和銷燬,但是這種情況下的準則極其簡單,就是在建構函式中分配資源,在解構函式中釋放資源,就好像飛機駕駛員必須在起飛後和降落前檢查起落架一樣。),即使在foo函式中發生了異常,str的生存期也會結束,C++保證自然退出時發生的一切在異常發生時一樣會有效。
auto_ptr只是智慧指標的一種,它的複製行為提供了所有權轉移的語義,即智慧指標在複製時將對內部維護的實際指標的所有權進行了轉移,例如
auto_ptr <string> str1( new string( <str1> ) ); cout << str1->c_str(); auto_ptr <string> str2(str1); // str1內部指標不再指向原來的物件 cout << str2->c_str(); cout << str1->c_str(); // 未定義,str1內部指標不再有效
某些時候,需要共享同一個物件,此時auto_ptr就不敷使用,由於某些歷史的原因,C++的標準庫中並沒有提供其他形式的智慧指標,走投無路了嗎?
另一種智慧指標
但是我們可以自己製作另一種形式的智慧指標,也就是具有值複製語義的,並且共享值的智慧指標。
需要同一個類的多個物件同時擁有一個物件的拷貝時,我們可以使用引用計數(Reference Counting/Using Counting)來實現,曾經這是一個C++中為了提高效率與COW(copy on write,改寫時複製)技術一起被廣泛使用的技術,後來證明在多執行緒應用中,COW為了保證行為的正確反而導致了效率降低(Herb Shutter的在C++ Report雜誌中的Guru專欄以及整理後出版的《More Exceptional C++》中專門討論了這個問題)。
然而對於我們目前的問題,引用計數本身並不會有太大的問題,因為沒有牽涉到複製問題,為了保證多執行緒環境下的正確,並不需要過多的效率犧牲,但是為了簡化問題,這裡忽略了對於多執行緒安全的考慮。
首先我們仿造auto_ptr設計了一個類别範本(出自Herb Shutter的《More Execptional C++》),
template <typename T> class shared_ptr { private: class implement // 實現類,引用計數 { public: implement(T* pp):p(pp),refs(1){} ~implement(){delete p;} T* p; // 實際指標 size_t refs; // 引用計數 }; implement* _impl; public: explicit shared_ptr(T* p) : _impl(new implement(p)){} ~shared_ptr() { decrease(); // 計數遞減 } shared_ptr(const shared_ptr& rhs) : _impl(rhs._impl) { increase(); // 計數遞增 } shared_ptr& operator=(const shared_ptr& rhs) { if (_impl != rhs._impl) // 避免自賦值 { decrease(); // 計數遞減,不再共享原物件 _impl=rhs._impl; // 共享新的物件 increase(); // 計數遞增,維護正確的引用計數值 } return *this; } T* operator->() const { return _impl->p; } T& operator*() const { return *(_impl->p); } private: void decrease() { if (--(_impl->refs)==0) { // 不再被共享,銷燬物件 delete _impl; } } void increase() { ++(_impl->refs); } };
這個類别範本是如此的簡單,所以都不需要對程式碼進行太多地說明。這裡僅僅給出一個簡單的使用例項,足以說明shared_ptr作為簡單的垃圾回收器的替代品。
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()函式中,先呼叫foo1(val),函式中使用了一個區域性物件temp,它和val共享同一份資料,並修改了實際值,函式返回後,val擁有的值同樣也發生了變化,而實際上val本身並沒有修改過。
然後呼叫了foo2(val),函式中使用了一個無名的臨時物件建立了一個新值,使用賦值表示式修改了val,同時val和臨時物件擁有同一個值,函式返回時,val仍然擁有這正確的值。
最後,在整個過程中,除了在使用shared_ptr 的建構函式時使用了new表示式建立新之外,並沒有任何刪除指標的動作,但是所有的記憶體管理均正確無誤,這就是得益於shared_ptr的精巧的設計。
擁有了auto_ptr和shared_ptr兩大利器以後,應該足以應付大多數情況下的垃圾回收了,如果你需要更復雜語義(主要是指複製時的語義)的智慧指標,可以參考boost的原始碼,其中設計了多種型別的智慧指標。
標準容器
對於需要在程式中擁有相同型別的多個物件,善用標準庫提供的各種容器類,可以最大限度的杜絕顯式的記憶體管理,然而標準容器並不適用於儲存指標,這樣對於多型性的支援仍然面臨困境。
使用智慧指標作為容器的元素型別,然而標準容器和演算法大多數需要值複製語義的元素,前面介紹的轉移所有權的auto_ptr和自制的共享物件的shared_ptr都不能提供正確的值複製語義,Herb Sutter在《More Execptional C++》中設計了一個具有完全複製語義的智慧指標ValuePtr,解決了指標用於標準容器的問題。
然而,多型性仍然沒有解決,我將在另一篇文章專門介紹使用容器管理多型物件的問題。
語言支援
為什麼不在C++語言中增加對垃圾回收的支援?
根據前面的討論,我們可以看見,不同的應用環境,也許需要不同的垃圾回收器,不管三七二十一使用垃圾回收,需要將這些不同型別的垃圾回收器整合在一起,即使可以成功(對此我感到懷疑),也會導致效率成本的增加。
這違反了C++的設計哲學,“不為不必要的功能支付代價”,強迫使用者接受垃圾回收的代價並不可取。
相反,按需選擇你自己需要的垃圾回收器,需要掌握的規則與顯式的管理記憶體相比,簡單的多,也不容易出錯。
最關鍵的一點, C++並不是“傻瓜型”的程式語言,他青睞喜歡和善於思考的程式設計者,設計一個合適自己需要的垃圾回收器,正是對喜愛C++的程式設計師的一種挑戰。
相關文章
- javascript的垃圾回收機制指的是什麼?JavaScript
- javascript的垃圾回收機制指的是什麼JavaScript
- java垃圾回收機制Java
- js垃圾回收機制JS
- javascript 垃圾回收機制JavaScript
- JVM 垃圾回收機制JVM
- Java 垃圾回收機制Java
- JVM垃圾回收機制JVM
- 剖析垃圾回收機制(上)
- Python垃圾回收機制Python
- java垃圾回收機制整理Java
- JavaScript的垃圾回收機制JavaScript
- PHP的垃圾回收機制PHP
- jvm的垃圾回收機制JVM
- java JVM垃圾回收機制JavaJVM
- jvm垃圾回收機制 一JVM
- jvm垃圾回收機制 二JVM
- 理解 Java 垃圾回收機制Java
- Java的垃圾回收機制Java
- JS的垃圾回收機制JS
- Python垃圾回收機制是什麼?有哪些優缺點?Python
- PHP的垃圾回收機制-回收週期PHP
- [效能][JVM]jvm垃圾回收機制JVM
- Flutter中的垃圾回收機制Flutter
- V8垃圾回收機制
- 【翻譯】PHP 垃圾回收機制PHP
- 圖解Golang垃圾回收機制!圖解Golang
- JS垃圾回收機制筆記JS筆記
- JVM垃圾回收機制入門JVM
- PHP 垃圾回收機制詳解PHP
- JVM垃圾回收機制(Garbage Collection)JVM
- Java 垃圾回收機制概念梳理Java
- 談談 JVM 垃圾回收機制JVM
- JAVA垃圾回收機制和Python垃圾回收對比與分析JavaPython
- Java虛擬機器 —— 垃圾回收機制Java虛擬機
- Java的垃圾回收(Garbage Collection)機制Java
- 秒懂JVM的垃圾回收機制JVM
- 聊聊JVM的垃圾回收機制GCJVMGC