C++ 爲何不加入垃圾回收機制

Java的愛好者們常常批評C++中沒有提供與Java相似的垃圾回收(Gabage Collector)機制(這很正常,正如C++的愛好者有時也攻擊Java沒有這個沒有那個,或者這個不行那個不夠好),致使C++中對動態存儲的官吏稱爲程序員的噩夢,不是嗎?你常常聽到的是內存遺失(memory leak)和非法指針存取,這必定令你很頭疼,並且你又不能拋棄指針帶來的靈活性。ios

在本文中,我並不想揭露Java提供的垃圾回收機制的天生缺陷,而是指出了C++中引入垃圾回收的可行性。請讀者注意,這裏介紹的方法更多的是基於當前標準和庫設計的角度,而不是要求修改語言定義或者擴展編譯器。程序員

什麼是垃圾回收?算法

做爲支持指針的編程語言,C++將動態管理存儲器資源的便利性交給了程序員。在使用指針形式的對象時(請注意,因爲引用在初始化後不能更改引用目標的語言機制的限制,多態性應用大多數狀況下依賴於指針進行),程序員必須本身完成存儲器的分配、使用和釋放,語言自己在此過程當中不能提供任何幫助,也許除了按照你的要求正確的和操做系統親密合做,完成實際的存儲器管理。標準文本中,屢次提到了「未定義(undefined)」,而這大多數狀況下和指針相關。編程

某些語言提供了垃圾回收機制,也就是說程序員僅負責分配存儲器和使用,而由語言自己負責釋放再也不使用的存儲器,這樣程序員就從討厭的存儲器管理的工做中脫身了。然而C++並無提供相似的機制,C++的設計者Bjarne Stroustrup在我所知的惟一一本介紹語言設計的思想和哲學的著做《The Design and Evolution of C++》(中譯本:C++語言的設計和演化)中花了一個小節討論這個特性。簡而言之,Bjarne本人認爲,安全

「我有意這樣設計C++,使它不依賴於自動垃圾回收(一般就直接說垃圾回收)。這是基於本身對垃圾回收系統的經驗,我很懼怕那種嚴重的空間和時間開銷,也懼怕因爲實現和移植垃圾回收系統而帶來的複雜性。還有,垃圾回收將使C++不適合作許多底層的工做,而這卻正是它的一個設計目標。但我喜歡垃圾回收的思想,它是一種機制,可以簡化設計、排除掉許多產生錯誤的根源。多線程

須要垃圾回收的基本理由是很容易理解的:用戶的使用方便以及比用戶提供的存儲管理模式更可靠。而反對垃圾回收的理由也有不少,但都不是最根本的,而是關於實現和效率方面的。編程語言

已經有充分多的論據能夠反駁:每一個應用在有了垃圾回收以後會作的更好些。相似的,也有充分的論據能夠反對:沒有應用可能由於有了垃圾回收而作得更好。函數

並非每一個程序都須要永遠無休止的運行下去;並非全部的代碼都是基礎性的庫代碼;對於許多應用而言,出現一點存儲流失是能夠接受的;許多應用能夠管理本身的存儲,而不須要垃圾回收或者其餘與之相關的技術,如引用計數等。ui

個人結論是,從原則上和可行性上說,垃圾回收都是須要的。可是對今天的用戶以及廣泛的使用和硬件而言,咱們還沒法承受將C++的語義和它的基本庫定義在垃圾回收系統之上的負擔。」this

以我之見,統一的自動垃圾回收系統沒法適用於各類不一樣的應用環境,而又不至於致使實現上的負擔。稍後我將設計一個針對特定類型的可選的垃圾回收器,能夠很明顯地看到,或多或少老是存在一些效率上的開銷,若是強迫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>classstring{public:   string(constchar* cstr) { _data=newchar[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>classshared_ptr{private:  classimplement  // 實現類,引用計數  {  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(constshared_ptr& rhs)    :  _impl(rhs._impl)  {    increase();  // 計數遞增  }  shared_ptr&operator=(constshared_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> (newint(200) );}int main(){  shared_ptr<int> val(newint(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++的程序員的一種挑戰。

相關文章
相關標籤/搜索