窥见C++11智能指针
導語: C++指針的內存管理相信是大部分C++入門程序員的夢魘,受到Boost的啟發,C++11標準推出了智能指針,讓我們從指針的內存管理中釋放出來,幾乎消滅所有new和delete。既然智能指針如此強大,今天我們來一窺智能指針的原理以及在多線程操作中需要注意的細節。
在遠古時代,C++使用了指針這把雙刃劍,既可以讓程序員精確地控制堆上每一塊內存,也讓程序更容易發生crash,大大增加了使用指針的技術門檻。因此,從C++98開始便推出了auto_ptr,對裸指針進行封裝,讓程序員無需手動釋放指針指向的內存區域,在auto_ptr生命周期結束時自動釋放,然而,由于auto_ptr在轉移指針所有權后會產生野指針,導致程序運行時crash,如下面示例代碼所示:
因此在C++11又推出了unique_ptr、shared_ptr、weak_ptr三種智能指針,慢慢取代auto_ptr。
unique_ptr是auto_ptr的繼承者,對于同一塊內存只能有一個持有者,而unique_ptr和auto_ptr唯一區別就是unique_ptr不允許賦值操作,也就是不能放在等號的右邊(函數的參數和返回值例外),這一定程度避免了一些誤操作導致指針所有權轉移,然而,unique_str依然有提供所有權轉移的方法move,調用move后,原unique_ptr就會失效,再用其訪問裸指針也會發生和auto_ptr相似的crash,如下面示例代碼,所以,即使使用了unique_ptr,也要慎重使用move方法,防止指針所有權被轉移。
除了上述用法,unique_ptr還支持創建動態數組。在C++中,創建數組有很多方法,如下所示:
這里需要注意的是,不管vector還是unique_ptr,雖然可以幫我們自動釋放數組內存,但如果數組的元素是復雜數據類型時,我們還需要在其析構函數中正確釋放內存。
auto_ptr和unique_ptr都有或多或少的缺陷,因此C++11還推出了shared_ptr,這也是目前工程內使用最多最廣泛的智能指針,他使用引用計數(感覺有參考Objective-C的嫌疑),實現對同一塊內存可以有多個引用,在最后一個引用被釋放時,指向的內存才釋放,這也是和unique_ptr最大的區別。
另外,使用shared_ptr過程中有幾點需要注意:
- 構造shared_ptr的方法,如下示例代碼所示,我們盡量使用shared_ptr構造函數或者make_shared的方式創建shared_ptr,禁止使用裸指針賦值的方式,這樣會shared_ptr難于管理指針的生命周期。
- 禁止使用指向shared_ptr的裸指針,也就是智能指針的指針,這聽起來就很奇怪,但開發中我們還需要注意,使用shared_ptr的指針指向一個shared_ptr時,引用計數并不會加一,操作shared_ptr的指針很容易就發生野指針異常。
- 使用shared_ptr創建動態數組,在介紹unique_ptr時我們就講過創建動態數組,而shared_ptr同樣可以做到,不過稍微復雜一點,如下代碼所示,除了要顯示指定析構方法外(因為默認是T的析構函數,不是T[]),另外對外的數據類型依然是shared_ptr<T>,非常有迷惑性,看不出來是數組,最后不能直接使用下標讀寫數組,要先get()獲取裸指針才可以使用下標。所以,不推薦使用shared_ptr來創建動態數組,盡量使用unique_ptr,這可是unique_ptr為數不多的優勢了。
- 用shared_ptr實現多態,在我們使用裸指針時,實現多態就免不了定義虛函數,那么用shared_ptr時也不例外,不過有一處是可以省下的,就是析構函數我們不需要定義為虛函數了,如下面代碼所示:
- 循環引用,筆者最先接觸引用計數的語言就是Objective-C,而OC中最常出現的內存問題就是循環引用,如下面代碼所示,A中引用B,B中引用A,spa和spb的強引用計數永遠大于等于1,所以直到程序退出前都不會被退出,這種情況有時候在正常的業務邏輯中是不可避免的,而解決循環引用的方法最有效就是改用weak_ptr,具體可見下一章。
正如上一章提到,使用shared_ptr過程中有可能會出現循環引用,關鍵原因是使用shared_ptr引用一個指針時會導致強引用計數+1,從此該指針的生命周期就會取決于該shared_ptr的生命周期,然而,有些情況我們一個類A里面只是想引用一下另外一個類B的對象,類B對象的創建不在類A,因此類A也無需管理類B對象的釋放,這個時候weak_ptr就應運而生了,使用shared_ptr賦值給一個weak_ptr不會增加強引用計數(strong_count),取而代之的是增加一個弱引用計數(weak_count),而弱引用計數不會影響到指針的生命周期,這就解開了循環引用,上一章最后的代碼使用weak_ptr可改造為如下代碼。
使用weak_ptr也有需要注意的點,因為既然weak_ptr不負責裸指針的生命周期,那么weak_ptr也無法直接操作裸指針,我們需要先轉化為shared_ptr,這就和OC的Strong-Weak Dance有點像了,具體操作如下:
看到這里,智能指針的用法基本介紹完了,后面筆者來粗淺地分析一下為什么智能指針可以有效幫我們管理裸指針的生命周期。
在C++中,內存會分為三部分,堆、棧和靜態存儲區,靜態存儲區會存放全局變量和靜態變量,在程序加載時就初始化,而堆是由程序員自行分配,自行釋放的,例如我們使用裸指針分配的內存;而最后棧是系統幫我們分配的,所以也會幫我們自動回收。因此,智能指針就是利用這一性質,通過一個棧上的對象(shared_ptr或unique_ptr)來管理一個堆上的對象(裸指針),在shared_ptr或unique_ptr的析構函數中判斷當前裸指針的引用計數情況來決定是否釋放裸指針。
[ boost中shared_ptr與weak_ptr類圖 ]
我們重點關注shared_ptr<T>的類圖,它就是我們可以直接操作的類,這里面包含裸指針T*,還有一個shared_count的對象,而shared_count對象還不是最終的引用計數,它只是包含了一個指向sp_counted_base的指針,這應該就是真正存放引用計數的地方,包括強應用計數和弱引用計數,而且shared_count中包含的是sp_counted_base的指針,不是對象,這也就意味著假如shared_ptr<T> a = b,那么a和b底層pi_指針指向的是同一個sp_counted_base對象,這就很容易做到多個shared_ptr的引用計數永遠保持一致了。
多個線程操作多個不同的shared_ptr對象
C++11中聲明了shared_ptr的計數操作具有原子性,不管是賦值導致計數增加還是釋放導致計數減少,都是原子性的,這個可以參考sp_counted_base的源碼,因此,基于這個特性,假如有多個shared_ptr共同管理一個裸指針,那么多個線程分別通過不同的shared_ptr進行操作是線程安全的。
多個線程操作同一個shared_ptr對象
同樣的道理,既然C++11只負責sp_counted_base的原子性,那么shared_ptr本身就沒有保證線程安全了,加入兩個線程同時訪問同一個shared_ptr對象,一個進行釋放(reset),另一個讀取裸指針的值,那么最后的結果就不確定了,很有可能發生野指針訪問crash。
總結
以上是生活随笔為你收集整理的窥见C++11智能指针的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 游戏数据埋点二三事
- 下一篇: s3c2440移植MQTT