大家好,今天借助本文,從實(shí)踐、避坑和實(shí)現(xiàn)原理三個(gè)角度分析下C++中的智能指針。
本文主要內(nèi)容如下圖所示:
智能指針的由來
auto_ptr為什么被廢棄
unique_ptr的使用、特點(diǎn)以及實(shí)現(xiàn)
shared_ptr的使用、特點(diǎn)以及實(shí)現(xiàn)
weak_ptr的使用、特點(diǎn)以及實(shí)現(xiàn)
介紹筆者在工作中遇到的一些職能指針相關(guān)的坑,并給出一些建議
背景
內(nèi)存的分配與回收都是由開發(fā)人員在編寫代碼時(shí)主動(dòng)完成的,好處是內(nèi)存管理的開銷較小,程序擁有更高的執(zhí)行效率;弊端是依賴于開發(fā)者的水平,隨著代碼規(guī)模的擴(kuò)大,極容易遺漏釋放內(nèi)存的步驟,或者一些不規(guī)范的編程可能會(huì)使程序具有安全隱患。如果對(duì)內(nèi)存管理不當(dāng),可能導(dǎo)致程序中存在內(nèi)存缺陷,甚至?xí)谶\(yùn)行時(shí)產(chǎn)生內(nèi)存故障錯(cuò)誤。換句話說,開發(fā)者自己管理內(nèi)存,最容易發(fā)生下面兩種情況:
申請了內(nèi)存卻沒有釋放,造成內(nèi)存泄漏
使用已經(jīng)釋放的內(nèi)存,造成segment fault
所以,為了在保證性能的前提下,又能使得開發(fā)者不需要關(guān)心內(nèi)存的釋放,進(jìn)而使得開發(fā)者能夠?qū)⒏嗟木ν度氲綐I(yè)務(wù)上,自C++11開始,STL正式引入了智能指針。
所有權(quán)
智能指針一個(gè)很關(guān)鍵的一個(gè)點(diǎn)就是是否擁有一個(gè)對(duì)象的所有權(quán),當(dāng)我們通過std::make_xxx或者new一個(gè)對(duì)象,那么就擁有了這個(gè)對(duì)象的所有權(quán)。
所有權(quán)分為獨(dú)占所有權(quán)、共享所有權(quán)以及弱共享所有權(quán)三種。
獨(dú)占所有權(quán)
顧名思義,獨(dú)占該對(duì)象。獨(dú)占的意思就是不共享,所有權(quán)可以轉(zhuǎn)移,但是轉(zhuǎn)移之后,所有權(quán)也是獨(dú)占。auto_ptr和unique_ptr就是一種獨(dú)占所有權(quán)方式的智能指針。
假設(shè)有個(gè)Object對(duì)象,如果A擁有該對(duì)象的話,就需要保證其在不使用該對(duì)象的時(shí)候,將該對(duì)象釋放;而此時(shí)如果B也想擁有Object對(duì)象,那么就必須先讓A放棄該對(duì)象所有權(quán),然后B獨(dú)享該對(duì)象,那么該對(duì)象的使用和釋放就只歸B所有,跟A沒有關(guān)系了。
獨(dú)占所有權(quán)具有以下幾個(gè)特點(diǎn):
如果創(chuàng)建或者復(fù)制了某個(gè)對(duì)象,就擁有了該對(duì)象
如果沒有創(chuàng)建對(duì)象,而是將對(duì)象保留使用,同樣擁有該對(duì)象的所有權(quán)
如果你擁有了某個(gè)對(duì)象的所有權(quán),在不需要某一個(gè)對(duì)象時(shí),需要釋放它們
共享所有權(quán)
共享所有權(quán),與獨(dú)占所有權(quán)正好相反,對(duì)某個(gè)對(duì)象的所有權(quán)可以共享。shared_ptr就是一種共享所有權(quán)方式的智能指針。
假設(shè)此時(shí)A擁有對(duì)象Object,在沒有其它擁有該對(duì)對(duì)象的情況下,對(duì)象的釋放由A來負(fù)責(zé);如果此時(shí)B也想擁有該對(duì)象,那么對(duì)象的釋放由最后一個(gè)擁有它的來負(fù)責(zé)。
舉一個(gè)我們經(jīng)常遇到的例子,socket連接,多個(gè)發(fā)送端(sender)可以使用其發(fā)送和接收數(shù)據(jù)。
弱共享所有權(quán)
弱共享所有權(quán),指的是可以使用該對(duì)象,但是沒有所有權(quán),由真正擁有其所有權(quán)的來負(fù)責(zé)釋放。weak_ptr就是一種弱共享所有權(quán)方式的智能指針。
分類
在C++11中,有unique_ptr、shared_ptr以及weak_ptr三種,auto_ptr因?yàn)樽陨磙D(zhuǎn)移所有權(quán)的原因,在C++11中被廢棄(本節(jié)最后,將簡單說下被廢棄的原因)。
unique_ptr
使用上限制最多的一種智能指針,被用來取代之前的auto_ptr,一個(gè)對(duì)象只能被一個(gè)unique_ptr所擁有,而不能被共享,如果需要將其所擁有的對(duì)象轉(zhuǎn)移給其他unique_ptr,則需要使用move語義
shared_ptr
與unique_ptr不同的是,unique_ptr是獨(dú)占管理權(quán),而shared_ptr則是共享管理權(quán),即多個(gè)shared_ptr可以共用同一塊關(guān)聯(lián)對(duì)象,其內(nèi)部采用的是引用計(jì)數(shù),在拷貝的時(shí)候,引用計(jì)數(shù)+1,而在某個(gè)對(duì)象退出作用域或者釋放的時(shí)候,引用計(jì)數(shù)-1,當(dāng)引用計(jì)數(shù)為0的時(shí)候,會(huì)自動(dòng)釋放其管理的對(duì)象。
weak_ptr
weak_ptr的出現(xiàn),主要是為了解決shared_ptr的循環(huán)引用,其主要是與shared_ptr一起來使用。和shared_ptr不同的地方在于,其并不會(huì)擁有資源,也就是說不能訪問對(duì)象所提供的成員函數(shù),不過,可以通過weak_ptr.lock()來產(chǎn)生一個(gè)擁有訪問權(quán)限的shared_ptr。
auto_ptr
auto_ptr自C++98被引入,因?yàn)槠浯嬖谳^多問題,所以在c++11中被廢棄,自C++17開始正式從STL中移除。
首先我們看下auto_ptr的簡單實(shí)現(xiàn)(為了方便閱讀,進(jìn)行了修改,基本功能類似于std::auto_ptr):
templateclassauto_ptr { T*p; public: auto_ptr(T*s):p(s){} ~auto_ptr(){deletep;} auto_ptr(auto_ptr&a){ p=a.p; a.p=NULL; } auto_ptr&operator=(auto_ptr&a){ deletep; p=a.p; a.p=NULL; return*this; } T&operator*()const{return*p;} T*operator->()const{returnp;} };
從上面代碼可以看出,auto_ptr采用copy語義來轉(zhuǎn)移所有權(quán),轉(zhuǎn)移之后,其關(guān)聯(lián)的資源指針設(shè)置為NULL,而這跟我們理解上copy行為不一致。
在<< Effective STL >>第8條,作者提出永不建立auto_ptr的容器,并以一個(gè)例子來說明原因,感興趣的可以去看看這本書,還是不錯(cuò)的。
實(shí)際上,auto_ptr被廢棄的直接原因是拷貝造成所有權(quán)轉(zhuǎn)移,如下代碼:
auto_ptra(newClassA); auto_ptr b=a; a->Method();
在上述代碼中,因?yàn)閎 = a導(dǎo)致所有權(quán)被轉(zhuǎn)移,即a關(guān)聯(lián)的對(duì)象為NULL,如果再調(diào)用a的成員函數(shù),顯然會(huì)造成coredump。
正是因?yàn)榭截悓?dǎo)致所有權(quán)被轉(zhuǎn)移,所以auto_ptr使用上有很多限制:
不能在STL容器中使用,因?yàn)閺?fù)制將導(dǎo)致數(shù)據(jù)無效
一些STL算法也可能導(dǎo)致auto_ptr失效,比如std::sort算法
不能作為函數(shù)參數(shù),因?yàn)檫@會(huì)導(dǎo)致復(fù)制,并且在調(diào)用后,導(dǎo)致原數(shù)據(jù)無效
如果作為類的成員變量,需要注意在類拷貝時(shí)候?qū)е碌臄?shù)據(jù)無效
正是因?yàn)閍uto_ptr的諸多限制,所以自C++11起,廢棄了auto_ptr,引入unique_ptr。
unique_ptr
unique_ptr是C++11提供的用于防止內(nèi)存泄漏的智能指針中的一種實(shí)現(xiàn)(用來替代auto_ptr),獨(dú)享被管理對(duì)象指針?biāo)袡?quán)的智能指針。
unique_ptr對(duì)象包裝一個(gè)原始指針,并負(fù)責(zé)其生命周期。當(dāng)該對(duì)象被銷毀時(shí),會(huì)在其析構(gòu)函數(shù)中刪除關(guān)聯(lián)的原始指針。具有->和*運(yùn)算符重載符,因此它可以像普通指針一樣使用。
分類
unique_ptr分為以下兩種:
指向單個(gè)對(duì)象
std::unique_ptrp1;//p1關(guān)聯(lián)Type對(duì)象
指向一個(gè)數(shù)組
unique_ptrp2;//p2關(guān)聯(lián)Type對(duì)象數(shù)組
特點(diǎn)
在前面的內(nèi)容中,我們已經(jīng)提到了unique_ptr的特點(diǎn),主要具有以下:
獨(dú)享所有權(quán),在作用域結(jié)束時(shí)候,自動(dòng)釋放所關(guān)聯(lián)的對(duì)象
voidfun(){ unique_ptra(newint(1)); }
無法進(jìn)行拷貝與賦值操作
unique_ptrptr(newint(1)); unique_ptr ptr1(ptr);//error unique_ptr ptr2=ptr;//error
顯示的所有權(quán)轉(zhuǎn)移(通過move語義)
unique_ptrptr(newint(1)); unique_ptr ptr1=std::move(ptr);//ok
作為容器元素存儲(chǔ)在容器中
unique_ptrptr(newint(1)); std::vector >v; v.push_back(ptr);//error v.push_back(std::move(ptr));//ok std::cout<*ptr?<
需要注意的是,自c++14起,可以使用下面的方式對(duì)unique_ptr進(jìn)行初始化:
autop1=std::make_unique(3.14); autop2=std::make_unique (n);
如果在c++11中使用上述方法進(jìn)行初始化,會(huì)得到下面的錯(cuò)誤提示:
error:‘make_unique’isnotamemberof‘std’
因此,如果為了使得c++11也可以使用std::make_unique,我們可以自己進(jìn)行封裝,如下:
namespacedetails{ #if__cplusplus>=201402L//C++14及以后使用STL實(shí)現(xiàn)的 usingstd::make_unique; #else templatestd::unique_ptr make_unique(Args&&...args) { returnstd::unique_ptr (newT(std::forward (args)...)); } #endif }//namespacedetails
使用
為了盡可能了解unique_ptr的使用姿勢,我們使用以下代碼為例:
#include#include //std::move voidfun1(double*); voidfun2(std::unique *); voidfun3(std::unique &); voidfun4(std::unique ); intmain(){ std::unique_ptr p(newdouble(3.14)); fun1(p.get()); fun2(&p); fun3(p); if(p){ std::cout<"is?valid"?<
上述代碼,基本覆蓋了常見的unique_ptr用法:
第10行,通過new創(chuàng)建一個(gè)unique_ptr對(duì)象
第11行,通過get()函數(shù)獲取其關(guān)聯(lián)的原生指針
第12行,通過unique_ptr對(duì)象的指針進(jìn)行訪問
第13行,通過unique_ptr對(duì)象的引用進(jìn)行訪問
第16行,通過if(p)來判斷其是否有效
第18行,通過release函數(shù)釋放所有權(quán),并將所有權(quán)進(jìn)行轉(zhuǎn)移
第19行,通過reset釋放之前的原生指針,并重新關(guān)聯(lián)一個(gè)新的指針
第20行,通過std::move轉(zhuǎn)移所有權(quán)
簡單實(shí)現(xiàn)
本部分只是基于源碼的一些思路,便于理解,實(shí)現(xiàn)的一個(gè)簡單方案,如果想要閱讀源碼,請點(diǎn)擊unique_ptr查看。
基本代碼如下:
templateclassunique_ptr { T*p; public: unique_ptr():p(){} unique_ptr(T*s):p(s){} ~unique_ptr(){deletep;} unique_ptr(constunique_ptr&)=delete; unique_ptr&operator=(constunique_ptr&)=delete; unique_ptr(unique_ptr&&s):p(s.p){s.p=nullptr} unique_ptr&operator=(unique_ptrs) {deletep;p=s.p;s.p=nullptr;return*this;} T*operator->()const{returnp;} T&operator*()const{return*p;} };
從上面代碼基本可以看出,unique_ptr的控制權(quán)轉(zhuǎn)移是通過move語義來實(shí)現(xiàn)的,相比于auto_ptr的拷貝語義轉(zhuǎn)移所有權(quán),更為合理。
shared_ptr
unique_ptr因?yàn)槠渚窒扌?獨(dú)享所有權(quán)),一般很少用于多線程操作。在多線程操作的時(shí)候,既可以共享資源,又可以自動(dòng)釋放資源,這就引入了shared_ptr。
shared_ptr為了支持跨線程訪問,其內(nèi)部有一個(gè)引用計(jì)數(shù)(線程安全),用來記錄當(dāng)前使用該資源的shared_ptr個(gè)數(shù),在結(jié)束使用的時(shí)候,引用計(jì)數(shù)為-1,當(dāng)引用計(jì)數(shù)為0時(shí),會(huì)自動(dòng)釋放其關(guān)聯(lián)的資源。
特點(diǎn)
相對(duì)于unique_ptr的獨(dú)享所有權(quán),shared_ptr可以共享所有權(quán)。其內(nèi)部有一個(gè)引用計(jì)數(shù),用來記錄共享該資源的shared_ptr個(gè)數(shù),當(dāng)共享數(shù)為0的時(shí)候,會(huì)自動(dòng)釋放其關(guān)聯(lián)的資源。
shared_ptr不支持?jǐn)?shù)組,所以,如果用shared_ptr指向一個(gè)數(shù)組的話,需要自己手動(dòng)實(shí)現(xiàn)deleter,如下所示:
std::shared_ptrp(newint[8],[](int*ptr){delete[]ptr;});
使用
仍然以一段代碼來說明,畢竟代碼更有說服力。
#include#include intmain(){ //創(chuàng)建shared_ptr對(duì)象 std::shared_ptr p1=std::make_shared (); *p1=78; std::cout<"p1?=?"?<*p1?<p2(p1); //打印引用計(jì)數(shù) std::cout<"p2?Reference?count?=?"?<
輸出如下:
p1=78 p1Referencecount=1 p2Referencecount=2 p1Referencecount=2 p1andp2arepointingtosamepointer Resetp1 p1ReferenceCount=0 p1ReferenceCount=1 p1ReferenceCount=0 p1isNULL
上面代碼基本羅列了shared_ptr的常用方法,對(duì)于其他方法,可以參考源碼或者官網(wǎng)。
線程安全
可能很多人都對(duì)shared_ptr是否線程安全存在疑惑,借助本節(jié),對(duì)線程安全方面的問題進(jìn)行分析和解釋。
shared_ptr的線程安全問題主要有以下兩種:
引用計(jì)數(shù)的加減操作是否線程安全
shared_ptr修改指向時(shí)是否線程安全
引用計(jì)數(shù)
shared_ptr中有兩個(gè)指針,一個(gè)指向所管理數(shù)據(jù)的地址,另一個(gè)一個(gè)指向執(zhí)行控制塊的地址。
執(zhí)行控制塊包括對(duì)關(guān)聯(lián)資源的引用計(jì)數(shù)以及弱引用計(jì)數(shù)等。在前面我們提到shared_ptr支持跨線程操作,引用計(jì)數(shù)變量是存儲(chǔ)在堆上的,那么在多線程的情況下,指向同一數(shù)據(jù)的多個(gè)shared_ptr在進(jìn)行計(jì)數(shù)的++或--時(shí)是否線程安全呢?
引用計(jì)數(shù)在STL中的定義如下:
_Atomic_word_M_use_count;//#shared _Atomic_word_M_weak_count;//#weak+(#shared!=0)
當(dāng)對(duì)shared_ptr進(jìn)行拷貝時(shí),引入計(jì)數(shù)增加,實(shí)現(xiàn)如下:
template<> inlinevoid _Sp_counted_base<_S_atomic>:: _M_add_ref_lock(){ //Performlock-freeadd-if-not-zerooperation. _Atomic_word__count; do { __count=_M_use_count; if(__count==0) __throw_bad_weak_ptr(); } while(!__sync_bool_compare_and_swap(&_M_use_count,__count, __count+1)); }
最終,計(jì)數(shù)的增加,是調(diào)用__sync_bool_compare_and_swap實(shí)現(xiàn)的,而該函數(shù)是線程安全的,因此我們可以得出結(jié)論:在多線程環(huán)境下,管理同一個(gè)數(shù)據(jù)的shared_ptr在進(jìn)行計(jì)數(shù)的增加或減少的時(shí)候是線程安全的,這是一波原子操作。
修改指向
修改指向分為操作同一個(gè)對(duì)象和操作不同對(duì)象兩種。
同一對(duì)象
以下面代碼為例:
voidfun(shared_ptr&p){ if(...){ p=p1; }else{ p=p2; } }
當(dāng)在多線程場景下調(diào)用該函數(shù)時(shí)候,p之前的引用計(jì)數(shù)要進(jìn)行-1操作,而p1對(duì)象的引用計(jì)數(shù)要進(jìn)行+1操作,雖然這倆的引用計(jì)數(shù)操作都是線程安全的,但是對(duì)這倆對(duì)象的引用計(jì)數(shù)的操作在一起時(shí)候卻不是線程安全的。這是因?yàn)楫?dāng)對(duì)p1的引用計(jì)數(shù)進(jìn)行+1時(shí)候,恰恰前一時(shí)刻,p1的對(duì)象被釋放,后面再進(jìn)行+1操作,會(huì)導(dǎo)致segment fault。
不同對(duì)象
代碼如下:
voidfun1(std::shared_ptr&p){ p=p1; } voidfun2(std::shared_ptr &p){ p=p2; } intmain(){ std::shared_ptr p=std::make_shared (); autop1=p; autop2=p; std::threadt1(fun1,p1); std::threadt2(fun2,p2); t1.join(); t2.join(); return0; }
在上述代碼中,p、p1、p2指向同一個(gè)資源,分別有兩個(gè)線程操作不同的shared_ptr對(duì)象(雖然關(guān)聯(lián)的底層資源是同一個(gè)),這樣在多線程下,只對(duì)p1和p2的引用計(jì)數(shù)進(jìn)行操作,不會(huì)引起segment fault,所以是線程安全的。
?
同一個(gè)shared_ptr被多個(gè)線程同時(shí)讀是安全的
同一個(gè)shared_ptr被多個(gè)線程同時(shí)讀寫是不安全的
?
簡單實(shí)現(xiàn)
本部分只是基于源碼的一些思路,便于理解,實(shí)現(xiàn)的一個(gè)簡單方案,如果想要閱讀源碼,請點(diǎn)擊shared_ptr查看。
記得之前看過一個(gè)問題為什么引用計(jì)數(shù)要new,這個(gè)問題我在面試的時(shí)候也問過,很少有人能夠回答出來,其實(shí),很簡單,因?yàn)橐С侄嗑€程訪問,所以只能要new呀。
代碼如下:
templateclassweak_ptr; classCounter{ public: Counter()=default; ints_=0;//shared_ptr的計(jì)數(shù) intw_=0;//weak_ptr的計(jì)數(shù) }; template classshared_ptr{ public: shared_ptr(T*p=0):ptr_(p){ cnt_=newCounter(); if(p){ cnt_->s_=1; } } ~shared_ptr(){ release(); } shared_ptr(shared_ptr const&s){ ptr_=s.ptr_; (s.cnt)->s_++; cnt_=s.cnt_; } shared_ptr(weakptr_ const&w){ ptr_=w.ptr_; (w.cnt_)->s_++; cnt_=w.cnt_; } shared_ptr &operator=(shared_ptr &s){ if(this!=&s){ release(); (s.cnt_)->s_++; cnt_=s.cnt_; ptr_=s.ptr_; } return*this; } T&operator*(){ return*ptr_; } T*operator->(){ returnptr_; } friendclassweak_ptr ; protected: voidrelease(){ cnt_->s_--; if(cnt_->s_1) ????{ ??????delete?ptr_; ??????if?(cnt_->w_1) ??????{ ??????????delete?cnt_; ??????????cnt_?=?NULL; ??????} ????} ??} private: ??T?*ptr_; ??Counter?*cnt_; };
weak_ptr
在三個(gè)智能指針中,weak_ptr是存在感最低的一個(gè),也是最容易被大家忽略的一個(gè)智能指針。它的引入是為了解決shared_ptr存在的一個(gè)問題循環(huán)引用。
特點(diǎn)
不具有普通指針的行為,沒有重載operator*和operator->
沒有共享資源,它的構(gòu)造不會(huì)引起引用計(jì)數(shù)增加
用于協(xié)助shared_ptr來解決循環(huán)引用問題
可以從一個(gè)shared_ptr或者另外一個(gè)weak_ptr對(duì)象構(gòu)造,進(jìn)而可以間接獲取資源的弱共享權(quán)。
使用
intmain(){ std::shared_ptrp1=std::make_shared (14); { std::weak_ptr weak=p1; std::shared_ptr new_shared=weak.lock(); shared_e1=nullptr; new_shared=nullptr; if(weak.expired()){ std::cout<"weak?pointer?is?expired"?<
上述代碼輸出如下:
weakpointerisexpired 0
使用成員函數(shù)use_count()和expired()來獲取資源的引用計(jì)數(shù),如果返回為0或者false,則表示關(guān)聯(lián)的資源不存在
使用lock()成員函數(shù)獲得一個(gè)可用的shared_ptr對(duì)象,進(jìn)而操作資源
當(dāng)expired()為true的時(shí)候,lock()函數(shù)將返回一個(gè)空的shared_ptr
簡單實(shí)現(xiàn)
templateclassweak_ptr { public: weak_ptr()=default; weak_ptr(shared_ptr &s):ptr_(s.ptr_),cnt(s.cnt_){ cnt_->w_++; } weak_ptr(weak_ptr &w):ptr_(w.ptr_),cnt_(w.cnt_){ cnt_->w_++; } ~weak_ptr(){ release(); } weak_ptr &operator=(weak_ptr &w){ if(this!=&w){ release(); cnt_=w.cnt_; cnt_->w_++; ptr_=w.ptr_; } return*this; } weak_ptr &operator=(shared_ptr &s) { release(); cnt_=s.cnt_; cnt_->w_++; ptr_=s.ptr_; return*this; } shared_ptr lock(){ returnshared_ptr (*this); } boolexpired(){ if(cnt){ if(cnt->s_>0){ returnfalse; } } returntrue; } friendclassshared_ptr ; protected: voidrelease(){ if(cnt_){ cnt_->w_--; if(cnt_->w_1?&&?cnt_->s_1)?{ ????????cnt_?=?nullptr; ??????} ????} ??} private: ????T?*ptr_?=?nullptr; ????Counter?*cnt_?=?nullptr; };
循環(huán)引用
在之前的文章內(nèi)存泄漏-原因、避免以及定位中,我們講到使用weak_ptr來配合shared_ptr使用來解決循環(huán)引用的問題,借助本文,我們深入說明下如何來解決循環(huán)引用的問題。
代碼如下:
classController{ public: Controller()=default; ~Controller(){ std::cout<"in?~Controller"?<controller_; }; std::shared_ptrsub_controller_; };
在上述代碼中,因?yàn)閏ontroller和sub_controller之間都有一個(gè)指向?qū)Ψ降膕hared_ptr,這樣就導(dǎo)致任意一個(gè)都因?yàn)閷?duì)方有一個(gè)指向自己的對(duì)象,進(jìn)而引用計(jì)數(shù)不能為0。
為了解決std::shared_ptr循環(huán)引用導(dǎo)致的內(nèi)存泄漏,我們可以使用std::weak_ptr來單面去除上圖中的循環(huán)。
classController{ public: Controller()=default; ~Controller(){ std::cout<"in?~Controller"?<controller_; }; std::shared_ptrsub_controller_; };
在上述代碼中,我們將SubController類中controller_的類型從std::shared_ptr變成std::weak_ptr。
那么,為什么將SubController中的shared_ptr換成weak_ptr就能解決這個(gè)問題呢?我們看下源碼:
template__weak_ptr& operator=(const__shared_ptr<_Tp1,?_Lp>&__r)//neverthrows { _M_ptr=__r._M_ptr; _M_refcount=__r._M_refcount; return*this; }
在上面代碼中,我們可以看到,將一個(gè)shared_ptr賦值給weak_ptr的時(shí)候,其引用計(jì)數(shù)并沒有+1,所以也就解決了循環(huán)引用的問題。
那么,如果我們想要使用shared_ptr關(guān)聯(lián)的對(duì)象進(jìn)行操作時(shí)候,該怎么做呢?使用weak_ptr::lock()函數(shù)來實(shí)現(xiàn),源碼如下:
__shared_ptr<_Tp,?_Lp> lock()const{ returnexpired()?__shared_ptr():__shared_ptr (*this); }
從上面代碼可看出,使用lock()函數(shù)生成一個(gè)shared_ptr供使用,如果之前的shared_ptr已經(jīng)被釋放,那么就返回一個(gè)空shared_ptr對(duì)象,否則生成shared_ptr對(duì)象的拷貝(這樣即使之前的釋放也不會(huì)存在問題)。
經(jīng)驗(yàn)之談
不要混用
指針之間的混用,有時(shí)候會(huì)造成不可預(yù)知的錯(cuò)誤,所以建議盡量不要混用。包括裸指針和智能指針以及智能指針之間的混用
裸指針和智能指針混用
代碼如下:
voidfun(){ autoptr=newType; std::shared_ptrt(ptr); deleteptr; }
在上述代碼中,將ptr所有權(quán)歸shared_ptr所擁有,所以在出fun()函數(shù)作用域的時(shí)候,會(huì)自動(dòng)釋放ptr指針,而在函數(shù)末尾又主動(dòng)調(diào)用delete來釋放,這就會(huì)造成double delete,會(huì)造成segment fault。
智能指針混用
代碼如下:
voidfun(){ std::unique_ptrt(newType); std::shared_ptr t1(t.get()); }
在上述代碼中,將t關(guān)聯(lián)的對(duì)象又給了t1,也就是說同一個(gè)對(duì)象被兩個(gè)智能指針?biāo)鶕碛?,所以在出fun()函數(shù)作用域的時(shí)候,二者都會(huì)釋放其關(guān)聯(lián)的對(duì)象,這就會(huì)造成double delete,會(huì)造成segment fault。
需要注意的是,下面代碼在STL中是支持的:
voidfun(){ std::unique_ptrt(newType); std::shared_ptr t1(std::move(t)); }
不要管理同一個(gè)裸指針
代碼如下:
voidfun(){ autoptr=newType; std::unique_ptrt(ptr); std::shared_ptr t1(ptr); }
在上述代碼中,ptr所有權(quán)同時(shí)給了t和t1,也就是說同一個(gè)對(duì)象被兩個(gè)智能指針?biāo)鶕碛?,所以在出fun()函數(shù)作用域的時(shí)候,二者都會(huì)釋放其關(guān)聯(lián)的對(duì)象,這就會(huì)造成double delete,會(huì)造成segment fault。
避免使用get()獲取原生指針
voidfun(){ autoptr=std::make_shared(); autoa=ptr.get(); std::shared_ptr t(a); deletea; }
一般情況下,生成的指針都要顯式調(diào)用delete來進(jìn)行釋放,而上述這種,很容易稍不注意就調(diào)用delete;非必要不要使用get()獲取原生指針。
不要管理this指針
classType{ private: voidfun(){ std::shared_ptrt(this); } };
在上述代碼中,如果Type在棧上,則會(huì)導(dǎo)致segment fault,堆上視實(shí)際情況(如果在對(duì)象在堆上生成,那么使用合理的話,是允許的)。
只管理堆上的對(duì)象
voidfun(){ Typet; std::shared_ptrptr(&t); };
在上述代碼中,t在棧上進(jìn)行分配,在出作用域的時(shí)候,會(huì)自動(dòng)釋放。而ptr在出作用域的時(shí)候,也會(huì)調(diào)用delete釋放t,而t本身在棧上,delete一個(gè)棧上的地址,會(huì)造成segment fault。
優(yōu)先使用unique_ptr
根據(jù)業(yè)務(wù)場景,如果需要資源獨(dú)占,那么建議使用unique_ptr而不是shared_ptr,原因如下:
性能優(yōu)于shared_ptr
因?yàn)閟hared_ptr在拷貝或者釋放時(shí)候,都需要操作引用計(jì)數(shù)
內(nèi)存占用上小于shared_ptr
shared_ptr需要維護(hù)它指向的對(duì)象的線程安全引用計(jì)數(shù)和一個(gè)控制塊,這使得它比unique_ptr更重量級(jí)
使用make_shared初始化
我們看下常用的初始化shared_ptr兩種方式,代碼如下:
std::shared_ptrp1=newType; std::shared_ptr p2=std::make_shared ();
那么,上述兩種方法孰優(yōu)孰劣呢?我們且從源碼的角度進(jìn)行分析。
第一種初始化方法,有兩次內(nèi)存分配:
new Type分配對(duì)象
為p1分配控制塊(control block),控制塊用于存放引用計(jì)數(shù)等信息
我們再看下make_shared源碼:
templateinline shared_ptr<_Ty>make_shared(_Types&&..._Args) {//makeashared_ptr _Ref_count_obj<_Ty>*_Rx= new_Ref_count_obj<_Ty>(_STDforward<_Types>(_Args)...); shared_ptr<_Ty>_Ret; _Ret._Resetp0(_Rx->_Getptr(),_Rx); return(_Ret); }
這里的_Ref_count_obj類包含成員變量:
控制塊
一個(gè)內(nèi)存塊,用于存放智能指針管理的資源對(duì)象
再看看_Ref_count_obj的構(gòu)造函數(shù):
template_Ref_count_obj(_Types&&..._Args) :_Ref_count_base() {//constructfromargumentlist ::new((void*)&_Storage)_Ty(_STDforward<_Types>(_Args)...); }
此處雖然也有一個(gè)new操作,但是此處是placement new,所以不存在內(nèi)存申請。
從上面分析我們可以看出,第一種初始化方式(new方式)共有兩次內(nèi)存分配操作,而第二種初始化方式(make_shared)只有一次內(nèi)存申請,所以建議使用make_shared方式進(jìn)行初始化。
結(jié)語
智能指針的出現(xiàn),能夠使得開發(fā)者不需要關(guān)心內(nèi)存的釋放,進(jìn)而使得開發(fā)者能夠?qū)⒏嗟木ν度氲綐I(yè)務(wù)上。但是,因?yàn)橹悄苤羔槺旧硪灿衅渚窒扌裕绻褂貌划?dāng),會(huì)造成意想不到的后果,所以,在使用之前,需要做一些必要的檢查,為了更好地用好智能指針,建議看下源碼實(shí)現(xiàn),還是比較簡單的。
審核編輯:彭靜
-
函數(shù)
+關(guān)注
關(guān)注
3文章
4260瀏覽量
62233 -
C++
+關(guān)注
關(guān)注
21文章
2090瀏覽量
73406 -
代碼
+關(guān)注
關(guān)注
30文章
4697瀏覽量
68086
原文標(biāo)題:智能指針-使用、避坑和實(shí)現(xiàn)
文章出處:【微信號(hào):C語言與CPP編程,微信公眾號(hào):C語言與CPP編程】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評(píng)論請先 登錄
相關(guān)推薦
評(píng)論