C++ 引用計數技術及智慧指標的簡單實現

melonstreet發表於2016-05-27

一直以來都對智慧指標一知半解,看C++Primer中也講的不夠清晰明白(大概是我功力不夠吧)。最近花了點時間認真看了智慧指標,特地來寫這篇文章。

1.智慧指標是什麼

簡單來說,智慧指標是一個類,它對普通指標進行封裝,使智慧指標類物件具有普通指標型別一樣的操作。具體而言,複製物件時,副本和原物件都指向同一儲存區域,如果通過一個副本改變其所指的值,則通過另一物件訪問的值也會改變.所不同的是,智慧指標能夠對記憶體進行進行自動管理,避免出現懸垂指標等情況。

2.普通指標存在的問題

C語言、C++語言沒有自動記憶體回收機制,關於記憶體的操作的安全性依賴於程式設計師的自覺。程式設計師每次new出來的記憶體塊都需要自己使用delete進行釋放,流程複雜可能會導致忘記釋放記憶體而造成記憶體洩漏。而智慧指標也致力於解決這種問題,使程式設計師專注於指標的使用而把記憶體管理交給智慧指標。

我們先來看看普通指標的懸垂指標問題。當有多個指標指向同一個基礎物件時,如果某個指標delete了該基礎物件,對這個指標來說它是明確了它所指的物件被釋放掉了,所以它不會再對所指物件進行操作,但是對於剩下的其他指標來說呢?它們還傻傻地指向已經被刪除的基礎物件並隨時準備對它進行操作。於是懸垂指標就形成了,程式崩潰也“指日可待”。我們通過程式碼+圖來來探求懸垂指標的解決方法。

程式碼簡單就不囉嗦解釋了。執行結果是輸出ptr2時並不是期待的1,因為1已經被刪除了。這個過程是這樣的:
C++ 引用計數技術及智慧指標的簡單實現

C++ 引用計數技術及智慧指標的簡單實現

C++ 引用計數技術及智慧指標的簡單實現

從圖可以看出,錯誤的產生來自於ptr1的”無知“:它並不知道還有其他指標共享著它指向的物件。如果有個辦法讓ptr1知道,除了它自己外還有兩個指標指向基礎物件,而它不應該刪除基礎物件,那麼懸垂指標的問題就得以解決了。如下圖:
C++ 引用計數技術及智慧指標的簡單實現

C++ 引用計數技術及智慧指標的簡單實現

那麼何時才可以刪除基礎物件呢?當然是只有一個指標指向基礎物件的時候,這時通過該指標就可以大大方方地把基礎物件刪除了。

3.什麼是引用計數

如何來讓指標知道還有其他指標的存在呢?這個時候我們該引入引用計數的概念了。引用計數是這樣一個技巧,它允許有多個相同值的物件共享這個值的實現。引用計數的使用常有兩個目的:

  • 簡化跟蹤堆中(也即C++中new出來的)的物件的過程。一旦一個物件通過呼叫new被分配出來,記錄誰擁有這個物件是很重要的,因為其所有者要負責對它進行delete。但是物件所有者可以有多個,且所有權能夠被傳遞,這就使得記憶體跟蹤變得困難。引用計數可以跟蹤物件所有權,並能夠自動銷燬物件。可以說引用計數是個簡單的垃圾回收體系。這也是本文的討論重點。
  • 節省記憶體,提高程式執行效率。如何很多物件有相同的值,為這多個相同的值儲存多個副本是很浪費空間的,所以最好做法是讓左右物件都共享同一個值的實現。C++標準庫中string類採取一種稱為”寫時複製“的技術,使得只有當字串被修改的時候才建立各自的拷貝,否則可能(標準庫允許使用但沒強制要求)採用引用計數技術來管理共享物件的多個物件。這不是本文的討論範圍。

4.智慧指標實現

瞭解了引用計數,我們可以使用它來寫我們的智慧指標類了。智慧指標的實現策略有兩種:輔助類與控制程式碼類。這裡介紹輔助類的實現方法。

4.1.基礎物件類

首先,我們來定義一個基礎物件類Point類,為了方便後面我們驗證智慧指標是否有效,我們為Point類建立如下介面:

4.2.輔助類

在建立智慧指標類之前,我們先建立一個輔助類。這個類的所有成員皆為私有型別,因為它不被普通使用者所使用。為了只為智慧指標使用,還需要把智慧指標類宣告為輔助類的友元。這個輔助類含有兩個資料成員:計數count與基礎物件指標。也即輔助類用以封裝使用計數與基礎物件指標。

4.3.為基礎物件類實現智慧指標類

引用計數是實現智慧指標的一種通用方法。智慧指標將一個計數器與類指向的物件相關聯,引用計數跟蹤共有多少個類物件共享同一指標。它的具體做法如下:

  • 當建立類的新物件時,初始化指標,並將引用計數設定為1
  • 當物件作為另一個物件的副本時,複製建構函式複製副本指標,並增加與指標相應的引用計數(加1)
  • 使用賦值操作符對一個物件進行賦值時,處理複雜一點:先使左運算元的指標的引用計數減1(為何減1:因為指標已經指向別的地方),如果減1後引用計數為0,則釋放指標所指物件記憶體。然後增加右運算元所指物件的引用計數(為何增加:因為此時做運算元指向物件即右運算元指向物件)。
  • 解構函式:呼叫解構函式時,解構函式先使引用計數減1,如果減至0則delete物件。

做好前面的準備後,我們可以來為基礎物件類Point書寫一個智慧指標類了。根據引用計數實現關鍵點,我們可以寫出我們的智慧指標類如下:

4.4.智慧指標類的使用與測試

至此,我們的智慧指標類就完成了,我們可以來看看如何使用

來看看執行結果咯:

如期,在離開大括號後,共享基礎物件的指標從3->2->1->0變換,最後計數為0時,pa物件被delete,此時使用getX()已經獲取不到原來的值。

5.智慧指標類的改進一

雖然我們的SmartPtr類稱為智慧指標,但它目前並不能像真正的指標那樣有->、*等操作符,為了使它看起來更像一個指標,我們來為它過載這些操作符。程式碼如下所示:

然後我們可以像指標般使用智慧指標類

6.智慧指標改進二

目前這個智慧指標智慧用於管理Point類的基礎物件,如果此時定義了個矩陣的基礎物件類,那不是還得重新寫一個屬於矩陣類的智慧指標類嗎?但是矩陣類的智慧指標類設計思想和Point類一樣啊,就不能借用嗎?答案當然是能,那就是使用模板技術。為了使我們的智慧指標適用於更多的基礎物件類,我們有必要把智慧指標類通過模板來實現。這裡貼上上面的智慧指標類的模板版:

好啦,現在我們能夠使用這個智慧指標類物件來共享其他型別的基礎物件啦,比如int:

執行結果如期所願,SmartPtr類管理起int型別來了:

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

C++ 引用計數技術及智慧指標的簡單實現 C++ 引用計數技術及智慧指標的簡單實現

相關文章