C++中智能指標如何設計和使用

來源:互聯網
上載者:User

智能指標(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;
}
};

實現引用計數有兩種經典策略,在這裡將使用其中一種,這裡所用的方法中,需要定義一個單獨的具體類用以封裝引用計數和相關指標:複製代碼 代碼如下:// 定義僅由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;
}
};

HasPtr類需要一個解構函式來刪除指標。但是,解構函式不能無條件的刪除指標。”
條件就是引用計數。如果該對象被兩個指標所指,那麼刪除其中一個指標,並不會調用該指標的解構函式,因為此時還有另外一個指標指向該對象。看來,智能指標主要是預防不當的析構行為,防止出現懸垂指標。

如所示,HasPtr就是智能指標,U_Ptr為計數器;裡面有個變數use和指標ip,use記錄了*ip對象被多少個HasPtr對象所指。假設現在又兩個HasPtr對象p1、p2指向了U_Ptr,那麼現在我delete p1,use變數將自減1, U_Ptr不會析構,那麼U_Ptr指向的對象也不會析構,那麼p2仍然指向了原來的對象,而不會變成一個懸null 指標。當delete p2的時候,use變數將自減1,為0。此時,U_Ptr對象進行析構,那麼U_Ptr指向的對象也進行析構,保證不會出現記憶體泄露。
包含指標的類需要特別注意複製控制,原因是複製指標時只複製指標中的地址,而不會複製指標指向的對象。
大多數C++類用三種方法之一管理指標成員
(1)不管指標成員。複製時只複製指標,不複製指標指向的對象。當其中一個指標把其指向的對象的空間釋放後,其它指標都成了懸浮指標。這是一種極端
(2)當複製的時候,即複製指標,也複製指標指向的對象。這樣可能造成空間的浪費。因為指標指向的對象的複製不一定是必要的。
(3) 第三種就是一種折中的方式。利用一個輔助類來管理指標的複製。原來的類中有一個指標指向輔助類,輔助類的資料成員是一個計數器和一個指標(指向原來的)(此為本次智能指標實現方式)。
其實,智能指標的引用計數類似於java的記憶體回收機制:java的垃圾的判定很簡答,如果一個對象沒有引用所指,那麼該對象為垃圾。系統就可以回收了。
HasPtr 智能指標的聲明如下,儲存一個指向U_Ptr對象的指標,U_Ptr對象指向實際的int基礎對象,代碼如下:複製代碼 代碼如下:#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;

運行結果如:

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.