C++中智慧指標的設計和使用

飛翔的黃瓜發表於2017-08-10

第一遍不太明白,存著慢慢看

  轉載請標明出處,原文地址:http://blog.csdn.net/hackbuteer1/article/details/7561235
     智慧指標(smart pointer)是儲存指向動態分配(堆)物件指標的類,用於生存期控制,能夠確保自動正確的銷燬動態分配的物件,防止記憶體洩露。它的一種通用實現技術是使用引用計數(reference count)。智慧指標類將一個計數器與類指向的物件相關聯,引用計數跟蹤該類有多少個物件共享同一指標。每次建立類的新物件時,初始化指標並將引用計數置為1;當物件作為另一物件的副本而建立時,拷貝建構函式拷貝指標並增加與之相應的引用計數;對一個物件進行賦值時,賦值操作符減少左運算元所指物件的引用計數(如果引用計數為減至0,則刪除物件),並增加右運算元所指物件的引用計數;呼叫解構函式時,建構函式減少引用計數(如果引用計數減至0,則刪除基礎物件)。
    智慧指標就是模擬指標動作的類。所有的智慧指標都會過載 -> 和 * 操作符。智慧指標還有許多其他功能,比較有用的是自動銷燬。這主要是利用棧物件的有限作用域以及臨時物件(有限作用域實現)解構函式釋放記憶體。當然,智慧指標還不止這些,還包括複製時可以修改源物件等。智慧指標根據需求不同,設計也不同(寫時複製,賦值即釋放物件擁有許可權、引用計數等,控制權轉移等)。auto_ptr 即是一種常見的智慧指標。
     智慧指標通常用類别範本實現:

[cpp] view plain copy
  1. template <class T>  
  2. class smartpointer  
  3. {  
  4. private:  
  5.     T *_ptr;  
  6. public:  
  7.     smartpointer(T *p) : _ptr(p)  //建構函式  
  8.     {  
  9.     }  
  10.     T& operator *()        //過載*操作符  
  11.     {  
  12.         return *_ptr;  
  13.     }  
  14.     T* operator ->()       //過載->操作符  
  15.     {  
  16.         return _ptr;  
  17.     }  
  18.     ~smartpointer()        //解構函式  
  19.     {  
  20.         delete _ptr;  
  21.     }  
  22. };  
實現引用計數有兩種經典策略,在這裡將使用其中一種,這裡所用的方法中,需要定義一個單獨的具體類用以封裝引用計數和相關指標:
[cpp] view plain copy
  1. // 定義僅由HasPtr類使用的U_Ptr類,用於封裝使用計數和相關指標  
  2. // 這個類的所有成員都是private,我們不希望普通使用者使用U_Ptr類,所以它沒有任何public成員  
  3. // 將HasPtr類設定為友元,使其成員可以訪問U_Ptr的成員  
  4. class U_Ptr  
  5. {  
  6.     friend class HasPtr;  
  7.     int *ip;  
  8.     size_t use;  
  9.     U_Ptr(int *p) : ip(p) , use(1)  
  10.     {  
  11.         cout << "U_ptr constructor called !" << endl;  
  12.     }  
  13.     ~U_Ptr()  
  14.     {  
  15.         delete ip;  
  16.         cout << "U_ptr distructor called !" << endl;  
  17.     }  
  18. };  
       HasPtr類需要一個解構函式來刪除指標。但是,解構函式不能無條件的刪除指標。”
      條件就是引用計數。
如果該物件被兩個指標所指,那麼刪除其中一個指標,並不會呼叫該指標的解構函式,因為此時還有另外一個指標指向該物件。看來,智慧指標主要是預防不當的析構行為,防止出現懸垂指標。

     如上圖所示,HasPtr就是智慧指標,U_Ptr為計數器;裡面有個變數use和指標ip,use記錄了*ip物件被多少個HasPtr物件所指。假設現在又兩個HasPtr物件p1、p2指向了U_Ptr,那麼現在我delete  p1,use變數將自減1,  U_Ptr不會析構,那麼U_Ptr指向的物件也不會析構,那麼p2仍然指向了原來的物件,而不會變成一個懸空指標。當delete p2的時候,use變數將自減1,為0。此時,U_Ptr物件進行析構,那麼U_Ptr指向的物件也進行析構,保證不會出現記憶體洩露。 
    包含指標的類需要特別注意複製控制,原因是複製指標時只複製指標中的地址,而不會複製指標指向的物件。
    大多數C++類用三種方法之一管理指標成員
    (1)不管指標成員。複製時只複製指標,不復制指標指向的物件。當其中一個指標把其指向的物件的空間釋放後,其它指標都成了懸浮指標。這是一種極端
    (2)當複製的時候,即複製指標,也複製指標指向的物件。這樣可能造成空間的浪費。因為指標指向的物件的複製不一定是必要的。
   (3) 第三種就是一種折中的方式。利用一個輔助類來管理指標的複製。原來的類中有一個指標指向輔助類,輔助類的資料成員是一個計數器和一個指標(指向原來的)(此為本次智慧指標實現方式)。
     其實,智慧指標的引用計數類似於java的垃圾回收機制:java的垃圾的判定很簡答,如果一個物件沒有引用所指,那麼該物件為垃圾。系統就可以回收了。
     HasPtr 智慧指標的宣告如下,儲存一個指向U_Ptr物件的指標,U_Ptr物件指向實際的int基礎物件,程式碼如下:
[cpp] view plain copy
  1. #include<iostream>  
  2. using namespace std;  
  3.   
  4. // 定義僅由HasPtr類使用的U_Ptr類,用於封裝使用計數和相關指標  
  5. // 這個類的所有成員都是private,我們不希望普通使用者使用U_Ptr類,所以它沒有任何public成員  
  6. // 將HasPtr類設定為友元,使其成員可以訪問U_Ptr的成員  
  7. class U_Ptr  
  8. {  
  9.     friend class HasPtr;  
  10.     int *ip;  
  11.     size_t use;  
  12.     U_Ptr(int *p) : ip(p) , use(1)  
  13.     {  
  14.         cout << "U_ptr constructor called !" << endl;  
  15.     }  
  16.     ~U_Ptr()  
  17.     {  
  18.         delete ip;  
  19.         cout << "U_ptr distructor called !" << endl;  
  20.     }  
  21. };  
  22.   
  23. class HasPtr  
  24. {  
  25. public:  
  26.     // 建構函式:p是指向已經動態建立的int物件指標  
  27.     HasPtr(int *p, int i) : ptr(new U_Ptr(p)) , val(i)  
  28.     {  
  29.         cout << "HasPtr constructor called ! " << "use = " << ptr->use << endl;  
  30.     }  
  31.   
  32.     // 複製建構函式:複製成員並將使用計數加1  
  33.     HasPtr(const HasPtr& orig) : ptr(orig.ptr) , val(orig.val)  
  34.     {  
  35.         ++ptr->use;  
  36.         cout << "HasPtr copy constructor called ! " << "use = " << ptr->use << endl;  
  37.     }  
  38.   
  39.     // 賦值操作符  
  40.     HasPtr& operator=(const HasPtr&);  
  41.   
  42.     // 解構函式:如果計數為0,則刪除U_Ptr物件  
  43.     ~HasPtr()  
  44.     {  
  45.         cout << "HasPtr distructor called ! " << "use = " << ptr->use << endl;  
  46.         if (--ptr->use == 0)  
  47.             delete ptr;  
  48.     }  
  49.   
  50.     // 獲取資料成員  
  51.     int *get_ptr() const  
  52.     {  
  53.         return ptr->ip;  
  54.     }  
  55.     int get_int() const  
  56.     {  
  57.         return val;  
  58.     }  
  59.   
  60.     // 修改資料成員  
  61.     void set_ptr(int *p) const  
  62.     {  
  63.         ptr->ip = p;  
  64.     }  
  65.     void set_int(int i)  
  66.     {  
  67.         val = i;  
  68.     }  
  69.   
  70.     // 返回或修改基礎int物件  
  71.     int get_ptr_val() const  
  72.     {  
  73.         return *ptr->ip;  
  74.     }  
  75.     void set_ptr_val(int i)  
  76.     {  
  77.         *ptr->ip = i;  
  78.     }  
  79. private:  
  80.     U_Ptr *ptr;   //指向使用計數類U_Ptr  
  81.     int val;  
  82. };  
  83. HasPtr& HasPtr::operator = (const HasPtr &rhs)  //注意,這裡賦值操作符在減少做運算元的使用計數之前使rhs的使用技術加1,從而防止自我賦值  
  84. {  
  85.     // 增加右運算元中的使用計數  
  86.     ++rhs.ptr->use;  
  87.     // 將左運算元物件的使用計數減1,若該物件的使用計數減至0,則刪除該物件  
  88.     if (--ptr->use == 0)  
  89.         delete ptr;  
  90.     ptr = rhs.ptr;   // 複製U_Ptr指標  
  91.     val = rhs.val;   // 複製int成員  
  92.     return *this;  
  93. }  
  94.   
  95. int main(void)  
  96. {  
  97.     int *pi = new int(42);  
  98.     HasPtr *hpa = new HasPtr(pi, 100);    // 建構函式  
  99.     HasPtr *hpb = new HasPtr(*hpa);     // 拷貝建構函式  
  100.     HasPtr *hpc = new HasPtr(*hpb);     // 拷貝建構函式  
  101.     HasPtr hpd = *hpa;     // 拷貝建構函式  
  102.   
  103.     cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl;  
  104.     hpc->set_ptr_val(10000);  
  105.     cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl;  
  106.     hpd.set_ptr_val(10);  
  107.     cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl;  
  108.     delete hpa;  
  109.     delete hpb;  
  110.     delete hpc;  
  111.     cout << hpd.get_ptr_val() << endl;  
  112.     return 0;  
  113. }  
這裡的賦值操作符比較麻煩,且讓我用圖表分析一番:
假設現在又兩個智慧指標p1、 p2,一個指向內容為42的記憶體,一個指向內容為100的記憶體,如下圖:

現在,我要做賦值操作,p2 = p1。對比著上面的
[cpp] view plain copy
  1. HasPtr& operator=(const HasPtr&);   // 賦值操作符  
此時,rhs就是p1,首先將p1指向的ptr的use加1,
[cpp] view plain copy
  1. ++rhs.ptr->use;     // 增加右運算元中的使用計數  
然後,做:
[cpp] view plain copy
  1. if (--ptr->use == 0)  
  2.         delete ptr;  
因為,原先p2指向的物件現在p2不在指向,那麼該物件就少了一個指標去指,所以,use做自減1;
此時,條件成立。因為u2的use為1。那麼,執行U_Ptr的解構函式,而在U_Ptr的解構函式中,做了delete ip操作,所以釋放了記憶體,不會有記憶體洩露的問題。
接下來的操作很自然,無需多言:

[cpp] view plain copy
  1. ptr = rhs.ptr;   // 複製U_Ptr指標  
  2.     val = rhs.val;   // 複製int成員  
  3.     return *this;  
做完賦值操作後,那麼就成為如下圖所示了。紅色標註的就是變化的部分:

而還要注意的是,過載賦值操作符的時候,一定要注意的是,檢查自我賦值的情況。
如圖所示:


此時,做p1 = p1的操作。那麼,首先u1.use自增1,為2;然後,u1.use自減1,為1。那麼就不會執行delete操作,剩下的操作都可以順利進行。按《C++ primer》說法,“這個賦值操作符在減少左運算元的使用計數之前使rhs的使用計數加1,從而防止自身賦值”。哎,反正我是那樣理解的。當然,賦值操作符函式中一來就可以按常規那樣:
[cpp] view plain copy
  1. if(this == &rhs)  
  2.         return *this;  
執行結果如下圖:

相關文章