1. 引入
C++語言中的動態內存分配沒有自動回收機制,動態開辟的空間需要用戶自己來維護,在出函數作用域或者程序正常退出前必須釋放掉。 即程序員每次 new 出來的內存都要手動 delete,否則會造成內存泄露, 有時我們已經非常謹慎了 , 然防不勝防:流程太復雜,程序員忘記 delete;異常導致程序過早退出,沒有執行delete的情況屢見不鮮。
1 void FunTest() 2 { 3 int *p = new int[10]; 4 FILE* pFile = fopen("1. txt", "w"); 5 if (pFile == NULL) 6 { 7 return; //如果pFile == NULL則p指向的空間得不到釋放 8 } 9 // DoSomethint() ; 10 if (p != NULL) 11 { 12 delete[] p; 13 p = NULL; 14 } 15 } 16 void FunTest2() //異常導致程序提前退出 17 { 18 int *p = new int[10]; 19 try 20 { 21 DoSomething(); 22 } 23 catch(. . .) 24 { 25 return; 26 } 27 delete[] p; 28 }
在前面的異常處理一節中已經提到過可以定義一個類來管理資源的分配與初始化,把釋放資源的部分交給析構函數來處理,即RAII(Resource Acquisition Is Initialization)直譯過來即“資源獲取即初始化”,是C++語言的一種管理資源、避免泄漏的慣用法。其實從字面譯過來的意思並不全面,因為它只提到了一個方面,還有另一個同樣重要的是空間釋放與資源回收。RAII:定義一個類來封裝資源的分配和釋放, 在構造函數中完成資源的分配和初始化, 在析構函數中完成資源的清理, 可以保證資源的正確初始化和釋放。C++的這種資源管理機制保證了內存空間的正確使用,在任何情況下,已構造的對象最終會銷毀,即它的析構函數最終會被調用。簡單的說,RAII 的做法是使用一個對象,在其構造時獲取資源,在對象生命期控制對資源的訪問使之始終保持有效,最后在對象析構的時候釋放資源。
RAII本身不是智能指針,它是一種規范,一種解決問題的思想。智能指針是RAII的一種應用,智能管理資源,可以像指針一樣使用,但它不是指針。
2. 智能指針總述
用智能指針便可以有效緩解上面提到的問題,本文主要講解常見的智能指針的用法。Boost庫的智能指針如下:(ps: 新的C++11標准中已經引入了unique_ptr/shared_ptr/weak_ptr)
對於編譯器來說,智能指針實際上是一個棧對象,並非指針類型,在棧對象生命周期即將結束時,智能指針通過析構函數釋放由它管理的堆內存。所有智能指針都重載了“operator->”和“operator*”操作符,直接返回對象的引用,用以操作對象。訪問智能指針原來的方法則使用“.”操作符。(智能指針包含了 reset() 方法,如果不傳遞參數(或者傳遞 NULL),則智能指針會釋放當前管理的內存。如果傳遞一個對象,則智能指針會釋放當前對象,來管理新傳入的對象。)
2.1 被拋棄的std::auto_ptr
std::auto_ptr 屬於 STL,當然在 namespace std 中,包含頭文件 #include<memory> 便可以使用。std::auto_ptr 能夠方便的管理單個堆內存對象。auto_ptr用於指向一個動態分配的對象指針,他的析構函數用於刪除所指對象的空間,以此達到對對象生存期的控制。 auto_ptr本質是管理權限的轉移。在進行賦值,拷貝構造時,會對控制權進行轉移。怎么理解呢?我們用圖來解釋其中的問題。
上圖中的拷貝構造與賦值操作就可以體現auto_ptr 管理資源的本質,為了更清楚的了解auto_ptr ,下面我將模擬實現auto_ptr 的管理機制:
1 template <class T> 2 class AutoPtr 3 { 4 public: 5 AutoPtr(T* ptr = NULL) 6 :_Ptr(ptr) 7 {} 8 AutoPtr(AutoPtr<T>& ap) 9 :_ptr(ap._ptr){ //始終只有一個對象管理一塊空間 10 ap._Ptr = NULL; 11 } 12 AutoPtr<T>& operator=(const AutoPtr<T>& ap) { 13 if (this != &ap) { //始終只有一個對象有管理這塊空間的權限 14 delete this->_Ptr; 15 this->_ptr = ap._Ptr; 16 ap._Ptr = NULL; 17 } 18 return *this; 19 } 20 ~AutoPtr() { 21 delete _ptr; 22 } 23 T& operator*() { 24 if (_ptr = NULL) { 25 throw ap; //對空指針解引用時拋出異常 26 } 27 return *_ptr; 28 } 29 T* operator->() { 30 if (_ptr = NULL) { 31 throw ap; //使用箭頭訪問空指針時拋出異常 32 } 33 return _ptr; 34 } 35 bool operator==(const AutoPtr<T>& ap) { 36 return _ptr == ap._Ptr; 37 } 38 bool operator!=(const AutoPtr<T>& ap) { 39 return _ptr != ap._Ptr; 40 } 41 void Reset(T* ptr = NULL) { //刪除原有指針_ptr並獲得指針ptr的所有權 42 if (_ptr != ptr) { 43 delete _ptr; 44 } 45 _ptr = ptr; 46 }
T* get() const; //返回原始對象的指針
T* release(); //放棄指針的所有權 記住 release() 函數不會釋放對象,僅僅歸還所有權。
void reset(T* ptr = NULL);//刪除原有指針並獲得指針的p的所有權 47 private: 48 T* _Ptr; 49 };
上面有兩點需要說明:
1,在對“*”進行重載時,如果返回值寫成”T ”,而不是“T &”。若出現下面賦值語句:“*P1 = 12”,則編譯不會通過。
2,在對“->”進行重載時, 對這條語句:”P1->A = 30” //P1.operator->()->A = 30; 即P1->->A;但是這個可讀性太差,編譯器進行了優化,P1->A;
總結:std::auto_ptr 可用來管理單個對象的內存,但是,請注意如下幾點:
1) 首先auto_ptr智能指針是個封裝好的類;
2) 盡量不要使用“operator=”。如果使用了,請不要再使用先前對象;
3) std::auto_ptr 最好不要當成參數傳遞(讀者可以自行寫代碼確定為什么不能);
4) 采用棧上的指針去管理堆上的內容,所以auto_ptr所管理的對象必須是new出來的,也不能是malloc出來的。(原因:在auto_ptr的實現機制中,采用的是delete 掉一個指針,該delete一方面是調用了指針所指對象的析構函數(這也是為什么采用智能指針,new了一個對象,但是不用delete的原因),另一方面釋放了堆空間的內存。)
使用場景總結:
1)不要使用auto_ptr對象保存指向靜態分配對象的指針,否則,當auto_ptr對象本身被撤銷的時候,它將試圖刪除指向非動態分配對象的指針,導致未定義的行為。
2)永遠不要使用兩個 auto_ptrs 對象指向同一對象,導致這個錯誤的一種明顯方式是,使用同一指針來初始化或者 reset 兩個不同的 auto_ptr對象。另一種導致這個錯誤的微妙方式可能是,使用一個 auto_ptr 對象的 get 函數的結果來初始化或者 reset另一個 auto_ptr 對象。(每個智能指針對象析構的時候,都會調用一次delete,導致堆空間內存被釋放兩次,然而這是不被允許的。)
3)不要使用 auto_ptr 對象保存指向動態分配數組的指針。當auto_ptr 對象被刪除的時候,它只釋放一個對象—它使用普通delete 操作符,而不用數組的 delete [] 操作符。
4)不要將 auto_ptr 對象存儲在容器中。容器要求所保存的類型定義復制和賦值操作符,使它們表現得類似於內置類型的操作符:在復制(或者賦值)之后,兩個對象必須具有相同值,auto_ptr 類不滿足這個要求。
使用一個 std::auto_ptr 的限制很多,還不能用來管理堆內存數組,如此多的限制就很容易導致問題。所以說它是一個帶有缺陷的設計,是一個“棄兒”。由於 std::auto_ptr 引發了諸多問題,一些設計並不是非常符合 C++ 編程思想,所以C++引入了下面 boost 庫的智能指針,boost 智能指針可以解決如上問題。
2.2 boost::scoped_ptr
boost庫發展的簡單介紹:在C++11標准出來之前,C++98標准中都一直只有一個智能指針auto_ptr,我們知道,這是一個失敗的設計。它的本質是管理權的轉移,這有許多問題。而這時就有一群人開始擴展C++標准庫的關於智能指針的部分,他們組成了boost社區,他們負責boost庫的開發和維護。其目的是為C++程序員提供免費的、同行審查的、可移植的程序庫。boost庫可以和C++標准庫完美的共同工作,並且為其提供擴展功能。現在的C++11標准庫的智能指針很大程度上“借鑒”了boost庫。
boost::scoped_ptr 屬於 boost 庫,定義在 namespace boost 中,包含頭文件#include<boost/smart_ptr.hpp> 便可以使用。scoped_ptr 跟 auto_ptr 一樣,可以方便的管理單個堆內存對象,特別的是,scoped_ptr 獨享所有權,避免了auto_ptr惱人的幾個問題。
scoped_ptr是一種簡單粗暴的設計,它本質就是防拷貝,避免出現管理權的轉移。這是它的最大特點,所以他的拷貝構造函數和賦值運算符重載函數都只是聲明而不定義,而且為了防止有的人在類外定義,所以將函數聲明為protected。但這也是它最大的問題所在,就是不能賦值拷貝,也就是說功能不全。但是這種設計比較高效、簡潔。沒有 release() 函數,不會導致先前的內存泄露問題。下面我也將模擬實現scoped_ptr的管理機制:
1 template<class T> 2 class ScopedPtr 3 { 4 public: 5 ScopedPtr(T* ptr = NULL) { 6 :_ptr(ptr) 7 cout << "ScopedPtr()" << end; 8 } 9 ~ScopedPtr(){ 10 delete _ptr; 11 cout << "~ScopedPtr()" << end; 12 } 13 T& operator* (){ 14 return *_ptr; 15 } 16 T* operator->() { 17 return _ptr; 18 } 19 bool operator==(const ScopedPtr<T>& sp) { 20 return _ptr == sp._ptr; 21 } 22 bool operator!=(const ScopedPtr<T>& sp) { 23 return _ptr != sp._ptr; 24 } 25 void Reset(T* ptr = NULL) 26 { 27 if (_ptr != ptr) 28 { 29 delete _ptr; 30 } 31 _ptr = ptr; 32 } 33 protected: 34 ScopedPtr(ScopedPtr<T>& sp); //防拷貝(只聲明不定義,為防止別人在類外定義,就將他聲明為protected) 35 ScopedPtr<T>& operator=(ScopedPtr<T>& sp); 36 private: 37 T* _ptr; 38 };
scoped_ptr使用特點總結:
1)與auto_ptr類似,采用棧上的指針去管理堆上的內容,從而使得堆上的對象隨着棧上對象銷毀時自動刪除;
2)scoped_ptr有着更嚴格的使用限制——不能拷貝,這也意味着scoped_ptr不能轉換其所有權,所以它管理的對象不能作為函數的返回值,對象生命周期僅僅局限於一定區間(該指針所在的{}區間,而std::auto_ptr可以);
3)由於防拷貝的特性,使其管理的對象不能共享所有權,這與std::auto_ptr類似,這一特點使該指針簡單易用,但也造成了功能的薄弱。