一直以來都對智慧指標一知半解,看C++Primer中也講的不夠清晰明白(大概是我功力不夠吧)。最近花了點時間認真看了智慧指標,特地來寫這篇文章。
1.智慧指標是什麼
簡單來說,智慧指標是一個類,它對普通指標進行封裝,使智慧指標類物件具有普通指標型別一樣的操作。具體而言,複製物件時,副本和原物件都指向同一儲存區域,如果通過一個副本改變其所指的值,則通過另一物件訪問的值也會改變.所不同的是,智慧指標能夠對記憶體進行進行自動管理,避免出現懸垂指標等情況。
2.普通指標存在的問題
C語言、C++語言沒有自動記憶體回收機制,關於記憶體的操作的安全性依賴於程式設計師的自覺。程式設計師每次new出來的記憶體塊都需要自己使用delete進行釋放,流程複雜可能會導致忘記釋放記憶體而造成記憶體洩漏。而智慧指標也致力於解決這種問題,使程式設計師專注於指標的使用而把記憶體管理交給智慧指標。
我們先來看看普通指標的懸垂指標問題。當有多個指標指向同一個基礎物件時,如果某個指標delete了該基礎物件,對這個指標來說它是明確了它所指的物件被釋放掉了,所以它不會再對所指物件進行操作,但是對於剩下的其他指標來說呢?它們還傻傻地指向已經被刪除的基礎物件並隨時準備對它進行操作。於是懸垂指標就形成了,程式崩潰也“指日可待”。我們通過程式碼+圖來來探求懸垂指標的解決方法。
1 2 3 4 5 6 7 8 9 10 11 |
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類建立如下介面:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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與基礎物件指標。也即輔助類用以封裝使用計數與基礎物件指標。
1 2 3 4 5 6 7 8 9 10 11 |
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書寫一個智慧指標類了。根據引用計數實現關鍵點,我們可以寫出我們的智慧指標類如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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.智慧指標類的使用與測試
至此,我們的智慧指標類就完成了,我們可以來看看如何使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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; } |
來看看執行結果咯:
1 2 3 4 |
還有2個指標指向基礎物件 還有1個指標指向基礎物件 -17891602 請按任意鍵繼續. . . |
如期,在離開大括號後,共享基礎物件的指標從3->2->1->0變換,最後計數為0時,pa物件被delete,此時使用getX()已經獲取不到原來的值。
5.智慧指標類的改進一
雖然我們的SmartPtr類稱為智慧指標,但它目前並不能像真正的指標那樣有->、*等操作符,為了使它看起來更像一個指標,我們來為它過載這些操作符。程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
{ 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; }; |
然後我們可以像指標般使用智慧指標類
1 2 3 4 |
Point *pa = new Point(10, 20); SmartPtr sptr1(pa); //像指標般使用 cout<<sptr1->getX(); |
6.智慧指標改進二
目前這個智慧指標智慧用於管理Point類的基礎物件,如果此時定義了個矩陣的基礎物件類,那不是還得重新寫一個屬於矩陣類的智慧指標類嗎?但是矩陣類的智慧指標類設計思想和Point類一樣啊,就不能借用嗎?答案當然是能,那就是使用模板技術。為了使我們的智慧指標適用於更多的基礎物件類,我們有必要把智慧指標類通過模板來實現。這裡貼上上面的智慧指標類的模板版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
//模板類作為友元時要先有宣告 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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型別來了:
1 2 3 4 5 |
2 20 還有2個指標指向基礎物件 還有1個指標指向基礎物件 請按任意鍵繼續. . . |
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式