C++中的資源管理(智能指針)

 C++中最重要, 也最令人頭疼的事情之一就是資源管理。原則上,每次在堆區用關鍵字new申請一塊內存,必須對應一個delete來釋放它。對應一般的小程序,通過細心地寫代碼,很容易做到。但當程序變大、變複雜的時候,僅僅依靠謹慎的編寫程序也是很難做到的。考慮以下幾種情況:

1. 用new申請資源與用delete釋放資源之間的跨度比較大,很容易在做完中間一系列的任務後忘記最初曾申請過資源,導致未釋放它。

2. 一個資源被多個變量引用,這樣每個引用結束後當然不能及時的釋放,因爲此時並不知道其他地方是否還在引用這個它。這樣也容易導致在最後一個引用後依然沒釋放資源。

3. 對一個資源的引用存在多個分支,因此要保證每個分支結束的地方對資源進行釋放。這樣一方面代碼變得不夠簡潔,更重要的是在以後的維護階段,每當有人想添加一個新分支時,可能全然不知這裏存在一個分配的資源,於是造成內存的泄漏。

4. 可能你真的做到了用delete來釋放資源,但在new 和delete之間的程序一旦有異常拋出,將不再按正常的順序往下執行,這樣依然會導致內存的泄漏。

因此我們需要一些更加高效、可靠的辦法,來對資源進行管理。

       比較流行的方法是用對象來管理資源。即一個對象中包含一個指針,對應我們申請的資源。但我們不直接操作指針,而是通過對象來間接的操作它。當對象超出作用域時,其析構函數通過調用delete來釋放資源,這樣我們將不用操心用完資源後釋放的問題。這種方法最簡單的模型如下:

[cpp] view plain copy
  1. template<typename T>  
  2. class MyPtr  
  3. {  
  4. public:  
  5.     MyPtr(T *p = NULL):m_ptr(p){}  
  6.     ~MyPtr()  
  7.     {  
  8.         if(m_ptr)  
  9.             delete m_ptr;  
  10.     }  
  11. private:  
  12.     T *m_ptr;  
  13. };  

這樣當我們要創建一個int型的指針時,可以這樣:

[cpp] view plain copy
  1. MyPtr<int> p(new int(6));  

用完之後不需要進行delete,因爲對象p析構時將會自動釋放。

       創建對象時,先申請我們需要的資源,作爲參數傳遞給對象的構造函數來初始化對象。這種技術在C++中叫“資源獲得即初始化”(RAII,Resource Acquisition Is Initialization)。當然,爲了更方便地操作指針,我們還需要定義一些成員來引用它,比如重載 *和->操作符。

[cpp] view plain copy
  1. template<typename T>  
  2. class MyPtr  
  3. {  
  4. public:  
  5.     MyPtr(T *p = NULL):m_ptr(p){}  
  6.     ~MyPtr()  
  7.     {  
  8.         if(m_ptr)  
  9.             delete m_ptr;  
  10.     }  
  11.     const T& operator * () const  
  12.     {  
  13.         return *m_ptr;  
  14.     }  
  15.     T& operator * ()  
  16.     {  
  17.         return *m_ptr;  
  18.     }  
  19.     const T* operator -> () const  
  20.     {  
  21.         return m_ptr;  
  22.     }  
  23.     T* operator -> ()  
  24.     {  
  25.         return m_ptr;  
  26.     }  
  27. private:  
  28.     T *m_ptr;  
  29. };  


這種方法很容易解決了資源釋放的問題,但是設想一下,當一個對象被複制了會怎麼樣?

此時多個對象同時指向了同一個資源,只要有一個對象被析構,該資源即同時被釋放。這樣其他對象將包含一個指向非法區域的指針!顯然,這種情況是絕對不允許的。

針對這個問題,有幾種解決辦法:

       第一種即在對象被複制時,顯式地將原對象中的指針設爲NULL,把新對象的指針指向該資源。換句話說,該資源的控制權由原對象轉移到新複製的對象身上,現在只有新對象可以操作該資源。有了這種硬性的規定,就不必擔心再次引用被釋放的資源的問題了。此時我們需要添加兩個成員(複製構造函數和重載=操作符)來控制複製操作,如下:

[cpp] view plain copy
  1. template<typename T>  
  2. class MyPtr  
  3. {  
  4. public:  
  5.     MyPtr(T *p = NULL):m_ptr(p){}  
  6.     MyPtr(MyPtr &mp):m_ptr(mp.m_ptr)  
  7.     {  
  8.         mp.m_ptr = NULL;  
  9.     }  
  10.     MyPtr& operator = (MyPtr &mp)  
  11.     {  
  12.         m_ptr = mp.m_ptr;  
  13.         mp.ptr = NULL;  
  14.     }  
  15.     ~MyPtr()  
  16.     {  
  17.         if(m_ptr)  
  18.             delete m_ptr;  
  19.     }  
  20.     const T& operator * () const  
  21.     {  
  22.         return *m_ptr;  
  23.     }  
  24.     T& operator * ()  
  25.     {  
  26.         return *m_ptr;  
  27.     }  
  28.     const T* operator -> () const  
  29.     {  
  30.         return m_ptr;  
  31.     }  
  32.     T* operator -> ()  
  33.     {  
  34.         return m_ptr;  
  35.     }  
  36. private:  
  37.     T *m_ptr;  
  38. };  

C++標準庫中,與些相對應的是auto_ptr,用法與上面一致。

       另一種解決方法,即對資源的引用進行計數。每當對象被複制時,引用計數加1。一個對象被析構時,計數減1。當計數減爲0時,再釋放資源。這樣就保證了對象複製後,不同的對象可以放心的使用資源,而不必擔心資源被其他對象提前釋放的問題了。這種技術被稱爲“智能指針”(Smart Pointer)。爲了實現引用計數,我們可以添加一個輔助類,對資源進行封裝並計數,如下:

[cpp] view plain copy
  1. template<typename T> class SmartPtr;  
  2. template<typename T>  
  3. class Ptr  
  4. {  
  5.     friend class SmartPtr<T>;  
  6.   
  7.     T *m_ptr;  
  8.     size_t m_count;  
  9.       
  10.     Ptr(T *p = NULL):m_ptr(p),m_count(1){}  
  11.     ~Ptr()  
  12.     {  
  13.         delete m_ptr;  
  14.     }  
  15. };  

智能指針的實現如下:

[cpp] view plain copy
  1. template<typename T>  
  2. class SmartPtr  
  3. {  
  4. public:  
  5.     SmartPtr(T *p = NULL):m_p(new Ptr<T>(p)){}  
  6.     SmartPtr(const SmartPtr& sp):m_p(sp.m_p)  
  7.     {  
  8.         ++m_p->m_count;  
  9.     }  
  10.   
  11.     SmartPtr& operator = (const SmartPtr& sp)  
  12.     {  
  13.         ++sp.m_p->m_count;  
  14.         if(--m_p->m_count == 0)  
  15.         {  
  16.             delete m_p;  
  17.         }  
  18.         m_p = sp.m_p;  
  19.   
  20.         return *this;  
  21.     }  
  22.   
  23.     T* operator ->() {   return m_p->m_ptr;   }  
  24.     const T* operator ->() const {   return m_p->m_ptr;   }  
  25.   
  26.     T operator *() {    return *m_p->m_ptr;  }  
  27.   
  28.     ~SmartPtr()  
  29.     {  
  30.         if(--m_p->m_count == 0)  
  31.             delete m_p;  
  32.     }  
  33.   
  34. private:  
  35.     Ptr<T> *m_p;  
  36. };  

對象的創建依然遵從RAII,初次創建的對象,引用爲1。所有複製的對象共享一個輔助類對象的指針。複製構造函數中,資源引用加1。析構對象時,先判斷引用是否變成0,是則釋放資源,否則保留。比較特殊的是重載=操作符,先對外面對象資源的引用加1,再自減自身資源的引用並判斷是否爲0,是則釋放自身資源,否則不釋放。最後重新指向新的資源。這樣也就同時實現了自我賦值的情況。

在boost庫中,與此對應的是shared_ptr。
      

       有了引用計數的智能指針,我們就可以放心使用、複製了。更重要的是我們可以在容器中存在它們,因爲容器的中元素都是複製的版本,像auto_ptr是不能放入容器的。

       總結:

       安全起見,我們應該儘可能地多使用RAII對象,而不是直接分配、操作資源,這樣可以在很大程度上減少內存泄漏的發生。C++標準庫中的auto_ptr和boost庫的shared_ptr,分別是RAII的兩種不同的類型。後者支持複製,適用於大多數情況。前者對象的複製本質上是資源控制權的轉移,複製後原對象的資源指針爲NULL,不能再對資源進行引用。應該根據不同的情況合理地選擇這兩種類型。


轉載自:http://blog.csdn.net/bonchoix/article/details/8044560

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章