C++智能指針(設計和使用)

前段時間學習project Anarchy引擎,底層很多都是用的只能指針,學了之後一直沒用過的,到現在終於算是見到實際作用了,現在重新溫故下

智能指針(smart pointer)是存儲指向動態分配(堆)對象指針的類,用於生存期控制,能夠確保自動正確的銷燬動態分配的對象,防止內存泄露。它的一種通用實現技術是使用引用計數(reference count)。智能指針類將一個計數器與類指向的對象相關聯,引用計數跟蹤該類有多少個對象共享同一指針。每次創建類的新對象時,初始化指針並將引用計數置爲1;當對象作爲另一對象的副本而創建時,拷貝構造函數拷貝指針並增加與之相應的引用計數;對一個對象進行賦值時,賦值操作符減少左操作數所指對象的引用計數(如果引用計數爲減至0,則刪除對象),並增加右操作數所指對象的引用計數;調用析構函數時,構造函數減少引用計數(如果引用計數減至0,則刪除基礎對象)。
    智能指針就是模擬指針動作的類。所有的智能指針都會重載 -> 和 * 操作符。智能指針還有許多其他功能,比較有用的是自動銷燬。這主要是利用棧對象的有限作用域以及臨時對象(有限作用域實現)析構函數釋放內存。當然,智能指針還不止這些,還包括複製時可以修改源對象等。智能指針根據需求不同,設計也不同(寫時複製,賦值即釋放對象擁有權限、引用計數等,控制權轉移等)。auto_ptr 即是一種常見的智能指針。

智能指針通常用類模板實現:
template <class T>
class smartpointer
{
private:
 T *_ptr;
public:
 smartpointer(T *p) : _ptr(p)  //構造函數
 {
 }
 T& operator *()        //重載*操作符
 {
  return *_ptr;
 }
 T* operator ->()       //重載->操作符
 {
  return _ptr;
 }
 ~smartpointer()        //析構函數
 {
  delete _ptr;
 }
};

 

實現引用計數有兩種經典策略,在這裏將使用其中一種,這裏所用的方法中,需要定義一個單獨的具體類用以封裝用計數和相關指針:

class U_Ptr 

    friend class HasPtr; 
    int *ip; 
    size_t use; 
    U_Ptr(int *p) : ip(p) , use(1) 
    { 
        cout << "U_ptr constructor called !" << endl; 
    } 
    ~U_Ptr() 
    { 
        delete ip; 
        cout << "U_ptr distructor called !" << endl; 
    } 
}; 

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基礎對象,代碼如下:

//////////////////////////////////////////////////////////////////////////
/// author: Jeson Yang
/// date:2014.9.17
//////////////////////////////////////////////////////////////////////////

 

#include<iostream> 
using namespace std; 

// 定義僅由HasPtr類使用的U_Ptr類,用於封裝使用計數和相關指針 
// 這個類的所有成員都是private,我們不希望普通用戶使用U_Ptr類,所以它沒有任何public成員 
// 將HasPtr類設置爲友元,使其成員可以訪問U_Ptr的成員 
class U_Ptr 

 friend class HasPtr; 
 int *ip; 
 size_t use; 
 U_Ptr(int *p) : ip(p) , use(1) 
 { 
  cout << "U_ptr constructor called !" << endl; 
 } 
 ~U_Ptr() 
 { 
  delete ip; 
  cout << "U_ptr distructor called !" << endl; 
 } 
}; 

class HasPtr 

public: 
 // 構造函數:p是指向已經動態創建的int對象指針 
 HasPtr(int *p, int i) : ptr(new U_Ptr(p)) , val(i) 
 { 
  cout << "HasPtr constructor called ! " << "use = " << ptr->use << endl; 
 } 

 // 複製構造函數:複製成員並將使用計數加1 
 HasPtr(const HasPtr& orig) : ptr(orig.ptr) , val(orig.val) 
 { 
  ++ptr->use; 
  cout << "HasPtr copy constructor called ! " << "use = " << ptr->use << endl; 
 } 

 // 賦值操作符 
 HasPtr& operator=(const HasPtr&); 

 // 析構函數:如果計數爲0,則刪除U_Ptr對象 
 ~HasPtr() 
 { 
  cout << "HasPtr distructor called ! " << "use = " << ptr->use << endl; 
  if (--ptr->use == 0) 
   delete ptr; 
 } 

 // 獲取數據成員 
 int *get_ptr() const 
 { 
  return ptr->ip; 
 } 
 int get_int() const 
 { 
  return val; 
 } 

 // 修改數據成員 
 void set_ptr(int *p) const 
 { 
  ptr->ip = p; 
 } 
 void set_int(int i) 
 { 
  val = i; 
 } 

 // 返回或修改基礎int對象 
 int get_ptr_val() const 
 { 
  return *ptr->ip; 
 } 
 void set_ptr_val(int i) 
 { 
  *ptr->ip = i; 
 } 
private: 
 U_Ptr *ptr;   //指向使用計數類U_Ptr 
 int val; 
}; 
HasPtr& HasPtr::operator = (const HasPtr &rhs)  //注意,這裏賦值操作符在減少做操作數的使用計數之前使rhs的使用技術加1,從而防止自我賦值 

 // 增加右操作數中的使用計數 
 ++rhs.ptr->use; 
 // 將左操作數對象的使用計數減1,若該對象的使用計數減至0,則刪除該對象 
 if (--ptr->use == 0) 
  delete ptr; 
 ptr = rhs.ptr;   // 複製U_Ptr指針 
 val = rhs.val;   // 複製int成員 
 return *this; 

int main(void) 

 int *pi = new int(42); 
 HasPtr *hpa = new HasPtr(pi, 100);    // 構造函數 
 HasPtr *hpb = new HasPtr(*hpa);     // 拷貝構造函數 
 HasPtr *hpc = new HasPtr(*hpb);     // 拷貝構造函數 
 HasPtr hpd = *hpa;     // 拷貝構造函數 

 cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl; 
 hpc->set_ptr_val(10000); 
 cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl; 
 hpd.set_ptr_val(10); 
 cout << hpa->get_ptr_val() << " " << hpb->get_ptr_val() << endl; 
 delete hpa; 
 delete hpb; 
 delete hpc; 
 cout << hpd.get_ptr_val() << endl; 
 return 0; 

這裏的賦值操作符比較麻煩,且讓我用圖表分析一番:
假設現在又兩個智能指針p1、 p2,一個指向內容爲42的內存,一個指向內容爲100的內存,如下圖:

現在,我要做賦值操作,p2 = p1。對比着上面的

HasPtr& operator=(const HasPtr&);   // 賦值操作符  

此時,rhs就是p1,首先將p1指向的ptr的use加1,

++rhs.ptr->use;     // 增加右操作數中的使用計數  

然後,做:

if (--ptr->use == 0) 
        delete ptr; 

因爲,原先p2指向的對象現在p2不在指向,那麼該對象就少了一個指針去指,所以,use做自減1;
此時,條件成立。因爲u2的use爲1。那麼,運行U_Ptr的析構函數,而在U_Ptr的析構函數中,做了delete ip操作,所以釋放了內存,不會有內存泄露的問題。
接下來的操作很自然,無需多言:

ptr = rhs.ptr;   // 複製U_Ptr指針 
    val = rhs.val;   // 複製int成員 
    return *this; 

 

做完賦值操作後,那麼就成爲如下圖所示了。紅色標註的就是變化的部分:

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


此時,做p1 = p1的操作。那麼,首先u1.use自增1,爲2;然後,u1.use自減1,爲1。那麼就不會執行delete操作,剩下的操作都可以順利進行。按《C++ primer》說法,“這個賦值操作符在減少左操作數的使用計數之前使rhs的使用計數加1,從而防止自身賦值”。哎,反正我是那樣理解的。當然,賦值操作符函數中一來就可以按常規那樣:

if(this == &rhs) 
        return *this;

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