0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

C++中的智能指針

電子工程師 ? 來源:C語(yǔ)言與CPP編程 ? 作者:C語(yǔ)言與CPP編程 ? 2022-08-05 11:11 ? 次閱讀

大家好,今天借助本文,從實(shí)踐、避坑和實(shí)現(xiàn)原理三個(gè)角度分析下C++中的智能指針。

本文主要內(nèi)容如下圖所示:

3490e1c0-1463-11ed-ba43-dac502259ad0.png

智能指針的由來

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ā)生下面兩種情況:

申請(qǐng)了內(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ù)。

34a87fe2-1463-11ed-ba43-dac502259ad0.png

弱共享所有權(quán)

弱共享所有權(quán),指的是可以使用該對(duì)象,但是沒有所有權(quán),由真正擁有其所有權(quán)的來負(fù)責(zé)釋放。weak_ptr就是一種弱共享所有權(quán)方式的智能指針。

34c2be70-1463-11ed-ba43-dac502259ad0.png

分類

在C++11中,有unique_ptr、shared_ptr以及weak_ptr三種,auto_ptr因?yàn)樽陨磙D(zhuǎn)移所有權(quán)的原因,在C++11中被廢棄(本節(jié)最后,將簡(jiǎn)單說下被廢棄的原因)。

unique_ptr

使用上限制最多的一種智能指針,被用來取代之前的auto_ptr,一個(gè)對(duì)象只能被一個(gè)unique_ptr所擁有,而不能被共享,如果需要將其所擁有的對(duì)象轉(zhuǎn)移給其他unique_ptr,則需要使用move語(yǔ)義

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的簡(jiǎn)單實(shí)現(xiàn)(為了方便閱讀,進(jìn)行了修改,基本功能類似于std::auto_ptr):

template
classauto_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語(yǔ)義來轉(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_ptrb=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_ptrptr1(ptr);//error
unique_ptrptr2=ptr;//error

顯示的所有權(quán)轉(zhuǎn)移(通過move語(yǔ)義)

unique_ptrptr(newint(1));
unique_ptrptr1=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<

需要注意的是,自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
template
std::unique_ptrmake_unique(Args&&...args)
{
returnstd::unique_ptr(newT(std::forward(args)...));
}
#endif
}//namespacedetails

使用

為了盡可能了解unique_ptr的使用姿勢(shì),我們使用以下代碼為例:

#include
#include//std::move

voidfun1(double*);
voidfun2(std::unique*);
voidfun3(std::unique&);
voidfun4(std::unique);

intmain(){
std::unique_ptrp(newdouble(3.14));

fun1(p.get());
fun2(&p);
fun3(p);

if(p){
std::cout<

上述代碼,基本覆蓋了常見的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)

簡(jiǎn)單實(shí)現(xiàn)

本部分只是基于源碼的一些思路,便于理解,實(shí)現(xiàn)的一個(gè)簡(jiǎn)單方案,如果想要閱讀源碼,請(qǐng)點(diǎn)擊unique_ptr查看。

基本代碼如下:

template
classunique_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語(yǔ)義來實(shí)現(xiàn)的,相比于auto_ptr的拷貝語(yǔ)義轉(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_ptrp1=std::make_shared();
*p1=78;
std::cout<p2(p1);
//打印引用計(jì)數(shù)
std::cout<

輸出如下:

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)在多線程場(chǎ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_ptrp=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í)讀寫是不安全的

?

簡(jiǎn)單實(shí)現(xiàn)

本部分只是基于源碼的一些思路,便于理解,實(shí)現(xiàn)的一個(gè)簡(jiǎn)單方案,如果想要閱讀源碼,請(qǐng)點(diǎn)擊shared_ptr查看。

記得之前看過一個(gè)問題為什么引用計(jì)數(shù)要new,這個(gè)問題我在面試的時(shí)候也問過,很少有人能夠回答出來,其實(shí),很簡(jiǎn)單,因?yàn)橐С侄嗑€程訪問,所以只能要new呀。

代碼如下:

template
classweak_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_ptrconst&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_w_

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_ptrweak=p1;
std::shared_ptrnew_shared=weak.lock();

shared_e1=nullptr;

new_shared=nullptr;
if(weak.expired()){
std::cout<

上述代碼輸出如下:

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

簡(jiǎn)單實(shí)現(xiàn)

template
classweak_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_ptrlock(){
returnshared_ptr(*this);
}

boolexpired(){
if(cnt){
if(cnt->s_>0){
returnfalse;
}
}
returntrue;
}

friendclassshared_ptr;

protected:
voidrelease(){
if(cnt_){
cnt_->w_--;
if(cnt_->w_s_

循環(huán)引用

在之前的文章內(nèi)存泄漏-原因、避免以及定位中,我們講到使用weak_ptr來配合shared_ptr使用來解決循環(huán)引用的問題,借助本文,我們深入說明下如何來解決循環(huán)引用的問題。

代碼如下:

classController{
public:
Controller()=default;

~Controller(){
std::cout<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。

34d2eb24-1463-11ed-ba43-dac502259ad0.png

為了解決std::shared_ptr循環(huán)引用導(dǎo)致的內(nèi)存泄漏,我們可以使用std::weak_ptr來單面去除上圖中的循環(huán)。

classController{
public:
Controller()=default;

~Controller(){
std::cout<controller_;
};

std::shared_ptrsub_controller_;
};

在上述代碼中,我們將SubController類中controller_的類型從std::shared_ptr變成std::weak_ptr。

34e5e008-1463-11ed-ba43-dac502259ad0.png

那么,為什么將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_ptrt1(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_ptrt1(std::move(t));
}

不要管理同一個(gè)裸指針

代碼如下:

voidfun(){
autoptr=newType;
std::unique_ptrt(ptr);
std::shared_ptrt1(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_ptrt(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ù)場(chǎng)景,如果需要資源獨(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_ptrp2=std::make_shared();

那么,上述兩種方法孰優(yōu)孰劣呢?我們且從源碼的角度進(jìn)行分析。

第一種初始化方法,有兩次內(nèi)存分配:

new Type分配對(duì)象

為p1分配控制塊(control block),控制塊用于存放引用計(jì)數(shù)等信息

我們?cè)倏聪耺ake_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)存申請(qǐng)。

從上面分析我們可以看出,第一種初始化方式(new方式)共有兩次內(nèi)存分配操作,而第二種初始化方式(make_shared)只有一次內(nèi)存申請(qǐng),所以建議使用make_shared方式進(jìn)行初始化。

結(jié)語(yǔ)

智能指針的出現(xiàn),能夠使得開發(fā)者不需要關(guān)心內(nèi)存的釋放,進(jìn)而使得開發(fā)者能夠?qū)⒏嗟木ν度氲綐I(yè)務(wù)上。但是,因?yàn)橹悄苤羔槺旧硪灿衅渚窒扌?,如果使用不?dāng),會(huì)造成意想不到的后果,所以,在使用之前,需要做一些必要的檢查,為了更好地用好智能指針,建議看下源碼實(shí)現(xiàn),還是比較簡(jiǎn)單的。

審核編輯:彭靜
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 函數(shù)
    +關(guān)注

    關(guān)注

    3

    文章

    4240

    瀏覽量

    62012
  • C++
    C++
    +關(guān)注

    關(guān)注

    21

    文章

    2085

    瀏覽量

    73323
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4674

    瀏覽量

    67819

原文標(biāo)題:智能指針-使用、避坑和實(shí)現(xiàn)

文章出處:【微信號(hào):C語(yǔ)言與CPP編程,微信公眾號(hào):C語(yǔ)言與CPP編程】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    淺談C/C++里的指針

    指針CC++的難點(diǎn)和重點(diǎn)。指針C的靈魂。我不想重復(fù)大多數(shù)書上說得很清楚的東西,只是把我看
    發(fā)表于 06-28 10:21 ?365次閱讀

    關(guān)于C++函數(shù)指針的使用

    關(guān)于C++函數(shù)指針的使用(包含對(duì)typedef用法的討論) (一)簡(jiǎn)單的函數(shù)指針的應(yīng)用。 //形式1:返回類型(*函數(shù)名)(參數(shù)表) char (*pFun)(int); c
    發(fā)表于 07-13 03:51

    嵌入式工程師需要哪些技能書/技能加點(diǎn)

    程師嵌入式硬件工程師通用技能樹軟件C/C++語(yǔ)言編程經(jīng)驗(yàn)、編程能力、應(yīng)用開發(fā)、OOP面向?qū)ο缶幊谭绞?、軟件框架?b class='flag-5'>C指針、文件IO、進(jìn)程線程、高級(jí)IO、對(duì)
    發(fā)表于 08-06 09:46

    華為內(nèi)部員工培訓(xùn)資料-C++課件

     C++語(yǔ)言中級(jí)教材講授C++語(yǔ)言的運(yùn)用技術(shù),包括:類、對(duì)象之間的關(guān)系、對(duì)象的存儲(chǔ)與布局、運(yùn)算符重載、智能指針、仿函數(shù)、泛型編程,C++模式設(shè)計(jì)基本思想。 目 &
    發(fā)表于 10-16 14:10 ?0次下載
    華為內(nèi)部員工培訓(xùn)資料-<b class='flag-5'>C++</b>課件

    C指針_CC++經(jīng)典著作

    C指針_CC++經(jīng)典著作,感興趣的小伙伴們可以瞧一瞧。
    發(fā)表于 11-16 18:32 ?0次下載

    C++實(shí)驗(yàn)--指針

    C++實(shí)驗(yàn)--指針
    發(fā)表于 12-30 14:50 ?1次下載

    C++指針”學(xué)習(xí)建議

    C++指針”學(xué)習(xí)建議
    發(fā)表于 03-31 15:53 ?3次下載

    為什么使用指針?C++的“指針

    為什么使用指針?因?yàn)樵诓僮鞔笮蛿?shù)據(jù)和類時(shí),指針可以通過內(nèi)存地址直接訪問數(shù)據(jù),可避免在程序復(fù)制大量的代碼,因此指針的效率最高。一般來說,指針
    的頭像 發(fā)表于 10-04 10:33 ?5031次閱讀

    C++指針的學(xué)習(xí)建議

     一。對(duì)于眾多人提出的c/c++指針難學(xué)的問題做個(gè)總結(jié):
    發(fā)表于 11-07 17:13 ?8次下載
    <b class='flag-5'>C++</b><b class='flag-5'>指針</b>的學(xué)習(xí)建議

    C++封裝:this指針

    C++封裝:this指針
    的頭像 發(fā)表于 06-29 14:37 ?3387次閱讀
    <b class='flag-5'>C++</b>封裝:this<b class='flag-5'>指針</b>

    【嵌入式技能樹】

    程師嵌入式硬件工程師通用技能樹軟件C/C++語(yǔ)言編程經(jīng)驗(yàn)、編程能力、應(yīng)用開發(fā)、OOP面向?qū)ο缶幊谭绞?、軟件框架?b class='flag-5'>C指針、文件IO、進(jìn)程線程、高級(jí)IO、對(duì)
    發(fā)表于 10-21 10:21 ?16次下載
    【嵌入式技能樹】

    CC++經(jīng)典著作《C指針

    CC++經(jīng)典著作《C指針
    發(fā)表于 01-17 09:46 ?0次下載

    C++中有函數(shù)指針還需要std::function嘛

    C/C++可以使用指針指向一段代碼,這個(gè)指針就叫函數(shù)指針,假設(shè)有這樣一段代碼:
    的頭像 發(fā)表于 02-15 14:13 ?438次閱讀
    <b class='flag-5'>C++</b>中有函數(shù)<b class='flag-5'>指針</b>還需要std::function嘛

    C++的引用和指針

    之前的文章我們已經(jīng)介紹了C++的基本類型如int,bool和double等,除了基本類型C++還有一些更復(fù)雜的數(shù)據(jù)類型復(fù)合類型,所謂的復(fù)合類型就是通過其他類型定義的類型,本篇文章我們將會(huì)著重介紹
    的頭像 發(fā)表于 03-17 14:00 ?554次閱讀

    C++智能指針的底層實(shí)現(xiàn)原理

    C++智能指針的頭文件: #include 1. shared_ptr: 智能指針從本質(zhì)上來說是一個(gè)模板類,用類實(shí)現(xiàn)對(duì)指針對(duì)象的管理。 template class shared_pt
    的頭像 發(fā)表于 11-09 14:32 ?593次閱讀
    <b class='flag-5'>C++</b><b class='flag-5'>智能指針</b>的底層實(shí)現(xiàn)原理