五点讲述C++智能指针的点点滴滴
(在學(xué)習(xí)C/C++或者想要學(xué)習(xí)C/C++可以加我們的學(xué)習(xí)交流QQ群:712263501群內(nèi)有相關(guān)學(xué)習(xí)資料)
0、摘要
本文先講了智能指針存在之前C++面臨的窘境,并順理成章地引出利用RAII技術(shù)封裝普通指針從而誕生了智能指針,然后以示例代碼的形式講解了三種智能指針的基本用法。為了更好地理解引用計(jì)數(shù)形式實(shí)現(xiàn)的智能指針,本文提供了實(shí)現(xiàn)一個(gè)簡單版本的智能指針的方法,并討論了引用計(jì)數(shù)形式的缺點(diǎn)。最后,本文討論了使用智能指針應(yīng)當(dāng)注意的事項(xiàng),包括shared_ptr 的循環(huán)引用問題等三個(gè)事項(xiàng)。
1、智能指針的前世今生
在智能指針出現(xiàn)以前,我們通常使用 new 和 delete 來管理動(dòng)態(tài)分配的內(nèi)存,但這種方式存在幾個(gè)常見的問題:
忘記 delete 內(nèi)存:會(huì)導(dǎo)致內(nèi)存泄漏問題,且除非是內(nèi)存耗盡否則很難檢測到這種錯(cuò)誤。
使用已經(jīng)釋放掉的對象:如果能夠記得在釋放掉內(nèi)存后將指針置空并在下次使用前判空,尚可避免這種錯(cuò)誤。
同一塊內(nèi)存釋放兩次:如果有兩個(gè)指針指向相同的動(dòng)態(tài)分配對象,則很容易發(fā)生這種錯(cuò)誤。
發(fā)生異常時(shí)的內(nèi)存泄漏:若在 new 和 delete 之間發(fā)生異常,則會(huì)導(dǎo)致內(nèi)存泄漏。
制造出這些錯(cuò)誤很容易,但查找和修正這些錯(cuò)誤就困難的多。于是,我們就要考慮如何從根本上克服這種弊端,不制造出這些錯(cuò)誤。動(dòng)態(tài)分配的內(nèi)存是 C++ 中最常使用的資源,所謂資源就是,一旦用了它,將來必須還給系統(tǒng),否則就會(huì)發(fā)生糟糕的事情。所以,我們就要考慮如何更好地進(jìn)行資源管理,來保證資源的有借必有還。
不難想到,資源管理技術(shù)的關(guān)鍵在于:要保證資源的釋放順序與獲取順序嚴(yán)格相反。這自然使我們聯(lián)想到局部對象的創(chuàng)建和銷毀過程。在C++中,定義在??臻g上的局部對象稱為自動(dòng)存儲(chǔ)對象。管理局部對象的任務(wù)非常簡單,因?yàn)樗鼈兊膭?chuàng)建和銷毀工作是由系統(tǒng)自動(dòng)完成的。我們只需在某個(gè)作用域中定義局部對象(這時(shí)系統(tǒng)自動(dòng)調(diào)用構(gòu)造函數(shù)以創(chuàng)建對象),然后就可以放心大膽地使用之,而不必?fù)?dān)心有關(guān)善后工作;當(dāng)控制流程超出這個(gè)作用域的范圍時(shí),系統(tǒng)會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù),從而銷毀該對象。
如果系統(tǒng)中的資源也具有如同局部對象一樣的特性,自動(dòng)獲取,自動(dòng)釋放,那該多么美妙啊!既然類是C++中的主要抽象工具,那么就將資源抽象為類,用局部對象來表示資源,把管理資源的任務(wù)轉(zhuǎn)化為管理局部對象的任務(wù)。把資源放進(jìn)對象內(nèi),用資源來管理對象,便是 C++ 編程中最重要的編程技法之一,即 RAII ,它是 “Resource Acquisition Is Initialization” 的首字母縮寫。智能指針便是利用 RAII 的技術(shù)對普通的指針進(jìn)行封裝,這使得智能指針實(shí)質(zhì)是一個(gè)對象,行為表現(xiàn)的卻像一個(gè)指針。
綜上所述,RAII的本質(zhì)內(nèi)容是用對象代表資源,把管理資源的任務(wù)轉(zhuǎn)化為管理對象的任務(wù),將資源的獲取和釋放與對象的構(gòu)造和析構(gòu)對應(yīng)起來,從而確保在對象的生存期內(nèi)資源始終有效,對象銷毀時(shí)資源必被釋放。換句話說,擁有對象就等于擁有資源,對象存在則資源必定存在。由此可見,RAII是進(jìn)行資源管理的有力武器。C++程序員依靠RAII寫出的代碼不僅簡潔優(yōu)雅,而且做到了異常安全。難怪微軟的MSDN雜志在最近的一篇文章中承認(rèn):“若論資源管理,誰也比不過標(biāo)準(zhǔn)C++”。
說到這里,順便 diss 一下 Java 的 GC 機(jī)制,表面來看,Java 似乎更優(yōu)秀,因?yàn)閺囊婚_始你就不用考慮什么特殊的機(jī)制,大膽地往前 new ,自有 GC 替你收拾殘局。 Java 的 GC 實(shí)際上是 JVM 中的一個(gè)獨(dú)立線程,采用不同的算法策略來收集堆中那些不再有引用指向的垃圾對象所占用的內(nèi)存。但是,通常情況下,GC 線程的優(yōu)先級比較低,只有在當(dāng)前程序空閑的時(shí)候才會(huì)被調(diào)度,收集垃圾。當(dāng)然,如果 JVM 感到內(nèi)存緊張了,JVM 會(huì)主動(dòng)調(diào)用 GC 來收集垃圾,獲取更多的內(nèi)存。請注意,Java 的 GC 工作的時(shí)機(jī)是:1. 當(dāng)前程序不忙,有空閑時(shí)間。2. 空閑內(nèi)存不足?,F(xiàn)在我們考慮一種常見的情況,程序在緊張運(yùn)行之中,沒有空閑時(shí)間給 GC 來運(yùn)行,同時(shí)機(jī)器內(nèi)存很大,JVM 也沒有感到內(nèi)存不足,結(jié)果是什么?對了 ,GC 形同虛設(shè),得不到調(diào)用。于是,內(nèi)存被不斷吞噬,而那些早已經(jīng)用不著的垃圾對象仍在在寶貴的內(nèi)存里睡大覺。
反過來看看 C++ 利用智能指針達(dá)成的效果,一旦某對象不再被引用,系統(tǒng)刻不容緩,立刻回收內(nèi)存。這通常發(fā)生在關(guān)鍵任務(wù)完成后的清理時(shí)期,不會(huì)影響關(guān)鍵任務(wù)的實(shí)時(shí)性,同時(shí),內(nèi)存里所有的對象都是有用的,絕對沒有垃圾空占內(nèi)存。
既然智能指針有如此多的好處,那我們還等什么,趕緊來學(xué)學(xué)它的用法吧!
2、智能指針的基本語法
C++11 中提供了三種智能指針,分別是 shared_ptr , unique_ptr 和 weak_ptr 。shared_ptr 允許多個(gè)指針指向同一個(gè)對象,unique_ptr 則“獨(dú)占”所指向的對象,weak_ptr 則是和share_ptr 相輔相成的伴隨類,具體用法后文細(xì)說。
這三種類型都定義在頭文件memory中。類似vector,智能指針也是模板,需要在尖括號(hào)內(nèi)給出類型信息。shared_ptr 和 unique_ptr 的使用方式和普通指針類似,都可以使用和->等運(yùn)算符。
關(guān)于基本語法,讀完下面這段代碼,你一定會(huì)了然于胸的。
以上代碼的輸出結(jié)果為:
為了更進(jìn)一步透徹地理解智能指針的基本原理,我們有必要實(shí)現(xiàn)一個(gè)簡單版本的智能指針(shared_ptr)來輔助理解。
3、自己實(shí)現(xiàn)一個(gè)簡單的智能指針
智能指針(shared_ptr)能夠自動(dòng)釋放所指向的對象,其實(shí)現(xiàn)原理卻并不復(fù)雜。簡單一說:
智能指針將一個(gè)計(jì)數(shù)器與類指向的對象相關(guān)聯(lián),引用計(jì)數(shù)跟蹤共有多少個(gè)類對象共享同一指針。
每次創(chuàng)建類的新對象時(shí),初始化指針并將引用計(jì)數(shù)置為1。
當(dāng)對象作為另一對象的副本而創(chuàng)建時(shí),拷貝構(gòu)造函數(shù)拷貝指針并增加與之相應(yīng)的引用計(jì)數(shù)。
對一個(gè)對象進(jìn)行賦值時(shí),賦值操作符減少左操作數(shù)所指對象的引用計(jì)數(shù)(如果引用計(jì)數(shù)為減至0,則刪除對象),并增加右操作數(shù)所指對象的引用計(jì)數(shù);這是因?yàn)樽髠?cè)的指針指向了右側(cè)指針?biāo)赶虻膶ο?#xff0c;因此右指針?biāo)赶虻膶ο蟮囊糜?jì)數(shù)加1。
調(diào)用析構(gòu)函數(shù)時(shí),構(gòu)造函數(shù)減少引用計(jì)數(shù)(如果引用計(jì)數(shù)減至0,則刪除基礎(chǔ)對象)。
下面是一個(gè)基于引用計(jì)數(shù)的智能指針的實(shí)現(xiàn),需要實(shí)現(xiàn)構(gòu)造,析構(gòu),拷貝構(gòu)造,=操作符重載,重載和->操作符。
這個(gè)智能指針的簡單實(shí)現(xiàn)模仿的是 share_ptr 的行為,不難發(fā)現(xiàn),引用計(jì)數(shù)的存在會(huì)帶來一些性能影響:
shared_ptr 的尺寸是裸指針的兩倍:因?yàn)閮?nèi)部既包含一個(gè)指向該資源的裸指針,也包含一個(gè)指向該資源的引用計(jì)數(shù)的裸指針。
引用計(jì)數(shù)的內(nèi)存必須動(dòng)態(tài)分配
引用計(jì)數(shù)的遞增和遞減必須是原子操作:原子操作一般比非原子操作慢。我們的實(shí)現(xiàn)版本里為了簡單起見沒有實(shí)現(xiàn)原子操作。
4、使用智能指針的一些注意事項(xiàng)
4.1 shared_ptr 的循環(huán)引用問題
shared_ptr 意味著你的引用和原對象是一個(gè)強(qiáng)聯(lián)系。你的引用不解開,原對象就不能銷毀。濫用強(qiáng)聯(lián)系,這在一個(gè)運(yùn)行時(shí)間長、規(guī)模比較大,或者是資源較為緊缺的系統(tǒng)中,極易造成隱性的內(nèi)存泄漏,這會(huì)成為一個(gè)災(zāi)難性的問題。
更糟的是,濫用強(qiáng)聯(lián)系可能造成循環(huán)引用的災(zāi)難。即:B持有指向A內(nèi)成員的一個(gè)shared_ptr,A也持有指向B內(nèi)成員的一個(gè) shared_ptr,此時(shí)A和B的生命周期互相由對方?jīng)Q定,事實(shí)上都無法從內(nèi)存中銷毀。 更進(jìn)一步,循環(huán)引用不只是兩方的情況,只要引用鏈成環(huán)都會(huì)出現(xiàn)問題。
舉個(gè)循環(huán)引用的簡單例子。
如此一來,A和B都互相指著對方吼,“放開我的引用!“,“你先發(fā)我的我就放你的!”,于是悲劇發(fā)生了,內(nèi)存泄漏了。當(dāng)然循環(huán)引用本身就說明設(shè)計(jì)上可能存在一些問題,如果特殊原因不得不使用循環(huán)引用,那可以讓引用鏈上的一方持用普通指針(或弱智能指針weak_ptr)即可。
這就是 weak_ptr 的用處。weak_ptr 提供一個(gè)(1)能夠確定對方生存與否(2)互相之間生命周期無干擾(3)可以臨時(shí)借用一個(gè)強(qiáng)引用(在你需要引用對方的短時(shí)間內(nèi)保證對方存活)的智能指針。而 weak_ptr 要求程序員在運(yùn)行時(shí)確定生存并加鎖,這也是邏輯上必須的本征復(fù)雜度——如果別人活的比你短,你當(dāng)然要:(1)先確定別人的死活(2)如果還活著,就給他續(xù)個(gè)命續(xù)到你用完了為止。
4.2 切記:讓所有的智能指針都有名字
智能指針為解決資源泄漏、編寫異常安全代碼提供了一種解決方案,那么他是萬能的良藥嗎?使用智能指針,就不會(huì)再有資源泄漏了嗎?請看下面的代碼:
上面的函數(shù)調(diào)用,看起來是安全的,但在現(xiàn)實(shí)世界中,其實(shí)不然:由于C++并未定義一個(gè)表達(dá)式的求值順序,因此上述函數(shù)調(diào)用除了func在最后得到調(diào)用之外是可以確定,其他的執(zhí)行序列則很可能被拆分成如下步驟:
1、分配內(nèi)存給T1
2、分配內(nèi)存給T2
3、構(gòu)造T1對象
4、構(gòu)造T2對象
5、構(gòu)造T1的智能指針對象
6、構(gòu)造T2的智能指針對象
7、調(diào)用func
此時(shí),如果程序在第3步失敗,那T1和T2對象所分配內(nèi)存必然泄漏。而解決這個(gè)問題的方案也很簡單,就是不要在函數(shù)實(shí)參中創(chuàng)建shared_ptr,拋棄臨時(shí)對象,讓所有的智能指針都有名字,就可以避免此類問題的發(fā)生。比如以下代碼:
4.3 優(yōu)先選用make_unique(shared)而非直接使用new
簡單說來,相比于直接使用new表達(dá)式,make系列函數(shù)有三個(gè)優(yōu)點(diǎn):
消除了重復(fù)代碼
改進(jìn)了異常安全性
生成的目標(biāo)代碼尺寸更小速度更快
總結(jié)
以上是生活随笔為你收集整理的五点讲述C++智能指针的点点滴滴的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: “不觉别时红泪尽”下一句是什么
- 下一篇: C 语言编程利器 之CLion