文章也發布在 騰訊雲+社區
一直以來都對智能指針一知半解,看C++Primer中也講的不夠清晰明白(大概是我功力不夠吧)。最近花了點時間認真看了智能指針,特地來寫這篇文章。
1.智能指針是什么
簡單來說,智能指針是一個類,它對普通指針進行封裝,使智能指針類對象具有普通指針類型一樣的操作。具體而言,復制對象時,副本和原對象都指向同一存儲區域,如果通過一個副本改變其所指的值,則通過另一對象訪問的值也會改變.所不同的是,智能指針能夠對內存進行進行自動管理,避免出現懸垂指針等情況。
2.普通指針存在的問題
C語言、C++語言沒有自動內存回收機制,關於內存的操作的安全性依賴於程序員的自覺。程序員每次new出來的內存塊都需要自己使用delete進行釋放,流程復雜可能會導致忘記釋放內存而造成內存泄漏。而智能指針也致力於解決這種問題,使程序員專注於指針的使用而把內存管理交給智能指針。
我們先來看看普通指針的懸垂指針問題。當有多個指針指向同一個基礎對象時,如果某個指針delete了該基礎對象,對這個指針來說它是明確了它所指的對象被釋放掉了,所以它不會再對所指對象進行操作,但是對於剩下的其他指針來說呢?它們還傻傻地指向已經被刪除的基礎對象並隨時准備對它進行操作。於是懸垂指針就形成了,程序崩潰也“指日可待”。我們通過代碼+圖來來探求懸垂指針的解決方法。
int * ptr1 = new int (1);
int * ptr2 = ptr1;
int * ptr3 = prt2;
cout << *ptr1 << endl;
cout << *ptr2 << endl;
cout << *ptr3 << endl;
delete ptr1;
cout << *ptr2 << endl;
代碼簡單就不啰嗦解釋了。運行結果是輸出ptr2時並不是期待的1,因為1已經被刪除了。這個過程是這樣的:
從圖可以看出,錯誤的產生來自於ptr1的”無知“:它並不知道還有其他指針共享着它指向的對象。如果有個辦法讓ptr1知道,除了它自己外還有兩個指針指向基礎對象,而它不應該刪除基礎對象,那么懸垂指針的問題就得以解決了。如下圖:
那么何時才可以刪除基礎對象呢?當然是只有一個指針指向基礎對象的時候,這時通過該指針就可以大大方方地把基礎對象刪除了。
3.什么是引用計數
如何來讓指針知道還有其他指針的存在呢?這個時候我們該引入引用計數的概念了。引用計數是這樣一個技巧,它允許有多個相同值的對象共享這個值的實現。引用計數的使用常有兩個目的:
- 簡化跟蹤堆中(也即C++中new出來的)的對象的過程。一旦一個對象通過調用new被分配出來,記錄誰擁有這個對象是很重要的,因為其所有者要負責對它進行delete。但是對象所有者可以有多個,且所有權能夠被傳遞,這就使得內存跟蹤變得困難。引用計數可以跟蹤對象所有權,並能夠自動銷毀對象。可以說引用計數是個簡單的垃圾回收體系。這也是本文的討論重點。
- 節省內存,提高程序運行效率。如何很多對象有相同的值,為這多個相同的值存儲多個副本是很浪費空間的,所以最好做法是讓左右對象都共享同一個值的實現。C++標准庫中string類采取一種稱為”寫時復制“的技術,使得只有當字符串被修改的時候才創建各自的拷貝,否則可能(標准庫允許使用但沒強制要求)采用引用計數技術來管理共享對象的多個對象。這不是本文的討論范圍。
4.智能指針實現
了解了引用計數,我們可以使用它來寫我們的智能指針類了。智能指針的實現策略有兩種:輔助類與句柄類。這里介紹輔助類的實現方法。
4.1.基礎對象類
首先,我們來定義一個基礎對象類Point類,為了方便后面我們驗證智能指針是否有效,我們為Point類創建如下接口:
class Point
{
public:
Point(int xVal = 0, int yVal = 0) :x(xVal), y(yVal) { }
int getX() const { return x; }
int getY() const { return y; }
void setX(int xVal) { x = xVal; }
void setY(int yVal) { y = yVal; }
private:
int x, y;
};
4.2.輔助類
在創建智能指針類之前,我們先創建一個輔助類。這個類的所有成員皆為私有類型,因為它不被普通用戶所使用。為了只為智能指針使用,還需要把智能指針類聲明為輔助類的友元。這個輔助類含有兩個數據成員:計數count與基礎對象指針。也即輔助類用以封裝使用計數與基礎對象指針。
class U_Ptr
{
private:
friend class SmartPtr;
U_Ptr(Point *ptr) :p(ptr), count(1) { }
~U_Ptr() { delete p; }
int count;
Point *p;
};
4.3.為基礎對象類實現智能指針類
引用計數是實現智能指針的一種通用方法。智能指針將一個計數器與類指向的對象相關聯,引用計數跟蹤共有多少個類對象共享同一指針。它的具體做法如下:
- 當創建類的新對象時,初始化指針,並將引用計數設置為1
- 當對象作為另一個對象的副本時,復制構造函數復制副本指針,並增加與指針相應的引用計數(加1)
- 使用賦值操作符對一個對象進行賦值時,處理復雜一點:先使左操作數的指針的引用計數減1(為何減1:因為指針已經指向別的地方),如果減1后引用計數為0,則釋放指針所指對象內存。然后增加右操作數所指對象的引用計數(為何增加:因為此時做操作數指向對象即右操作數指向對象)。
- 析構函數:調用析構函數時,析構函數先使引用計數減1,如果減至0則delete對象。
做好前面的准備后,我們可以來為基礎對象類Point書寫一個智能指針類了。根據引用計數實現關鍵點,我們可以寫出我們的智能指針類如下:
class SmartPtr
{
public:
SmartPtr(Point *ptr) :rp(new U_Ptr(ptr)) { }
SmartPtr(const SmartPtr &sp) :rp(sp.rp) { ++rp->count; }
SmartPtr& operator=(const SmartPtr& rhs) {
++rhs.rp->count;
if (--rp->count == 0)
delete rp;
rp = rhs.rp;
return *this;
}
~SmartPtr() {
if (--rp->count == 0)
delete rp;
else
cout << "還有" << rp->count << "個指針指向基礎對象" << endl;
}
private:
U_Ptr *rp;
};
4.4.智能指針類的使用與測試
至此,我們的智能指針類就完成了,我們可以來看看如何使用
int main()
{
//定義一個基礎對象類指針
Point *pa = new Point(10, 20);
//定義三個智能指針類對象,對象都指向基礎類對象pa
//使用花括號控制三個指針指針的生命期,觀察計數的變化
{
SmartPtr sptr1(pa);//此時計數count=1
{
SmartPtr sptr2(sptr1); //調用復制構造函數,此時計數為count=2
{
SmartPtr sptr3=sptr1; //調用賦值操作符,此時計數為conut=3
}
//此時count=2
}
//此時count=1;
}
//此時count=0;pa對象被delete掉
cout << pa->getX ()<< endl;
system("pause");
return 0;
}
來看看運行結果咯:
還有2個指針指向基礎對象
還有1個指針指向基礎對象
-17891602
請按任意鍵繼續. . .
如期,在離開大括號后,共享基礎對象的指針從3->2->1->0變換,最后計數為0時,pa對象被delete,此時使用getX()已經獲取不到原來的值。
5.智能指針類的改進一
雖然我們的SmartPtr類稱為智能指針,但它目前並不能像真正的指針那樣有->、*等操作符,為了使它看起來更像一個指針,我們來為它重載這些操作符。代碼如下所示:
{
public:
SmartPtr(Point *ptr) :rp(new U_Ptr(ptr)) { }
SmartPtr(const SmartPtr &sp) :rp(sp.rp) { ++rp->count; }
SmartPtr& operator=(const SmartPtr& rhs) {
++rhs.rp->count;
if (--rp->count == 0)
delete rp;
rp = rhs.rp;
return *this;
}
~SmartPtr() {
if (--rp->count == 0)
delete rp;
else
cout << "還有" << rp->count << "個指針指向基礎對象" << endl;
}
Point & operator *() //重載*操作符
{
return *(rp->p);
}
Point* operator ->() //重載->操作符
{
return rp->p;
}
private:
U_Ptr *rp;
};
然后我們可以像指針般使用智能指針類
Point *pa = new Point(10, 20);
SmartPtr sptr1(pa);
//像指針般使用
cout<<sptr1->getX();
6.智能指針改進二
目前這個智能指針智能用於管理Point類的基礎對象,如果此時定義了個矩陣的基礎對象類,那不是還得重新寫一個屬於矩陣類的智能指針類嗎?但是矩陣類的智能指針類設計思想和Point類一樣啊,就不能借用嗎?答案當然是能,那就是使用模板技術。為了使我們的智能指針適用於更多的基礎對象類,我們有必要把智能指針類通過模板來實現。這里貼上上面的智能指針類的模板版:
//模板類作為友元時要先有聲明
template <typename T>
class SmartPtr;
template <typename T>
class U_Ptr //輔助類
{
private:
//該類成員訪問權限全部為private,因為不想讓用戶直接使用該類
friend class SmartPtr<T>; //定義智能指針類為友元,因為智能指針類需要直接操縱輔助類
//構造函數的參數為基礎對象的指針
U_Ptr(T *ptr) :p(ptr), count(1) { }
//析構函數
~U_Ptr() { delete p; }
//引用計數
int count;
//基礎對象指針
T *p;
};
template <typename T>
class SmartPtr //智能指針類
{
public:
SmartPtr(T *ptr) :rp(new U_Ptr<T>(ptr)) { } //構造函數
SmartPtr(const SmartPtr<T> &sp) :rp(sp.rp) { ++rp->count; } //復制構造函數
SmartPtr& operator=(const SmartPtr<T>& rhs) { //重載賦值操作符
++rhs.rp->count; //首先將右操作數引用計數加1,
if (--rp->count == 0) //然后將引用計數減1,可以應對自賦值
delete rp;
rp = rhs.rp;
return *this;
}
T & operator *() //重載*操作符
{
return *(rp->p);
}
T* operator ->() //重載->操作符
{
return rp->p;
}
~SmartPtr() { //析構函數
if (--rp->count == 0) //當引用計數減為0時,刪除輔助類對象指針,從而刪除基礎對象
delete rp;
else
cout << "還有" << rp->count << "個指針指向基礎對象" << endl;
}
private:
U_Ptr<T> *rp; //輔助類對象指針
};
好啦,現在我們能夠使用這個智能指針類對象來共享其他類型的基礎對象啦,比如int:
int main()
{
int *i = new int(2);
{
SmartPtr<int> ptr1(i);
{
SmartPtr<int> ptr2(ptr1);
{
SmartPtr<int> ptr3 = ptr2;
cout << *ptr1 << endl;
*ptr1 = 20;
cout << *ptr2 << endl;
}
}
}
system("pause");
return 0;
}
運行結果如期所願,SmartPtr類管理起int類型來了:
2
20
還有2個指針指向基礎對象
還有1個指針指向基礎對象
請按任意鍵繼續. . .