C++11 智能指針

原作者:Babu_Abdulsalam 本文翻譯自CodeProject,轉(zhuǎn)載請(qǐng)注明出處。

引入###

Ooops. 盡管有另外一篇文章說(shuō)C++11里的智能指針了。近來(lái),我聽(tīng)到許多人談?wù)?code>C++新標(biāo)準(zhǔn),就是所謂的C++0x/C++11。 我研究了一下C++11的一些語(yǔ)言特性,發(fā)現(xiàn)確實(shí)它確實(shí)有一些巨大的改變。我將重點(diǎn)關(guān)注C++11的智能指針部分。

背景###

普通指針(normal/raw/naked pointers)的問(wèn)題?

讓我們一個(gè)接一個(gè)的討論。

如果不恰當(dāng)處理指針就會(huì)帶來(lái)許多問(wèn)題,所以人們總是避免使用它。這也是許多新手程序員不喜歡指針的原因。指針總是會(huì)扯上很多問(wèn)題,例如指針?biāo)赶驅(qū)ο蟮纳芷?,掛起引用?code>dangling references)以及內(nèi)存泄露。

如果一塊內(nèi)存被多個(gè)指針引用,但其中的一個(gè)指針釋放且其余的指針并不知道,這樣的情況下,就發(fā)生了掛起引用。而內(nèi)存泄露,就如你知道的一樣,當(dāng)從堆中申請(qǐng)了內(nèi)存后不釋放回去,這時(shí)就會(huì)發(fā)生內(nèi)存泄露。有人說(shuō),我寫了清晰并且?guī)в绣e(cuò)誤驗(yàn)證的代碼,為什么我還要使用智能指針呢?一個(gè)程序員也問(wèn)我:“嗨,下面是我的代碼,我從堆(heap)中申請(qǐng)了一塊內(nèi)存,使用完后,我又正確的把它歸還給了堆,那么使用智能指針的必要在哪里?”

void Foo( )
{ 
    int* iPtr = new int[5];  
    //manipulate the memory block . . .  
    delete[ ] iPtr;
 }

理想狀況下,上面這段代碼確實(shí)能夠工作的很好,內(nèi)存也能夠恰當(dāng)?shù)尼尫呕厝ァ5亲屑?xì)思考一下實(shí)際的工作環(huán)境以及代碼執(zhí)行條件。在內(nèi)存分配和釋放的間隙,程序指令確實(shí)能做許多糟糕的事情,比如訪問(wèn)無(wú)效的內(nèi)存地址,除以0,或者有另外一個(gè)程序員在你的程序中修改了一個(gè)bug,他根據(jù)一個(gè)條件增加了一個(gè)過(guò)早的返回語(yǔ)句。

在以上所有情況下,你的程序都走不到內(nèi)存釋放的那部分。前兩種情況下,程序拋出了異常,而第三種情況,內(nèi)存還沒(méi)釋放,程序就過(guò)早的return了。所以程序運(yùn)行時(shí),內(nèi)存就已經(jīng)泄露了。

解決以上所有問(wèn)題的方法就是使用智能指針[如果它們足夠智能的話]。

什么是智能指針?

智能指針是一個(gè)RAIIResource Acquisition is initialization)類模型,用來(lái)動(dòng)態(tài)的分配內(nèi)存。它提供所有普通指針提供的接口,卻很少發(fā)生異常。在構(gòu)造中,它分配內(nèi)存,當(dāng)離開(kāi)作用域時(shí),它會(huì)自動(dòng)釋放已分配的內(nèi)存。這樣的話,程序員就從手動(dòng)管理動(dòng)態(tài)內(nèi)存的繁雜任務(wù)中解放出來(lái)了。

C++98提供了第一種智能指針:auto_ptr

auto_ptr###

讓我們來(lái)見(jiàn)識(shí)一下auto_ptr如何解決上述問(wèn)題的吧。

class Test
{
    public: 
    Test(int a = 0 ) : m_a(a) { }
    ~Test( )
    { 
       cout << "Calling destructor" << endl; 
    }
    public: int m_a;
};
void main( )
{ 
    std::auto_ptr<Test> p( new Test(5) ); 
    cout << p->m_a << endl;
}  

上述代碼會(huì)智能地釋放與指針綁定的內(nèi)存。作用的過(guò)程是這樣的:我們申請(qǐng)了一塊內(nèi)存來(lái)放Test對(duì)象,并且把他綁定到auto_ptr p上。所以當(dāng)p離開(kāi)作用域時(shí),它所指向的內(nèi)存塊也會(huì)被自動(dòng)釋放。

//***************************************************************
class Test
{
public:
 Test(int a = 0 ) : m_a(a)
 {
 }
 ~Test( )
 {
  cout<<"Calling destructor"<<endl;
 }
public:
 int m_a;
};
//***************************************************************
void Fun( )
{
 int a = 0, b= 5, c;
 if( a ==0 )
 {
  throw "Invalid divisor";
 }
 c = b/a;
 return;
}
//***************************************************************
void main( )
{
 try
 {
  std::auto_ptr<Test> p( new Test(5) ); 
  Fun( );
  cout<<p->m_a<<endl;
 }
 catch(...)
 {
  cout<<"Something has gone wrong"<<endl;
 }
}

上面的例子中,盡管異常被拋出,但是指針仍然正確地被釋放了。這是因?yàn)楫?dāng)異常拋出時(shí),棧松綁(stack unwinding),當(dāng)try 塊中的所有對(duì)象destroy后,p 離開(kāi)了該作用域,所以它綁定的內(nèi)存也就釋放了。

Issue1:

目前為止,auto_ptr還是足夠智能的,但是它還是有一些根本性的破綻的。當(dāng)把一個(gè)auto_ptr賦給另外一個(gè)auto_ptr時(shí),它的所有權(quán)(ownship)也轉(zhuǎn)移了。當(dāng)我在函數(shù)間傳遞auto_ptr時(shí),這就是一個(gè)問(wèn)題。話說(shuō),我在Foo()中有一個(gè)auto_ptr,然后在Foo()中我把指針傳遞給了Fun()函數(shù),當(dāng)Fun()函數(shù)執(zhí)行完畢時(shí),指針的所有權(quán)不會(huì)再返還給Foo。

//***************************************************************
class Test
{
public:
 Test(int a = 0 ) : m_a(a)
 {
 }
 ~Test( )
 {
  cout<<"Calling destructor"<<endl;
 }
public:
 int m_a;
};
 
 
//***************************************************************
void Fun(auto_ptr<Test> p1 )
{
 cout<<p1->m_a<<endl;
}
//***************************************************************
void main( )
{
 std::auto_ptr<Test> p( new Test(5) ); 
 Fun(p);
 cout<<p->m_a<<endl;
} 

由于auto_ptr的野指針行為,上面的代碼導(dǎo)致程序崩潰。在這期間發(fā)生了這些細(xì)節(jié),p擁有一塊內(nèi)存,當(dāng)Fun調(diào)用時(shí), p把關(guān)聯(lián)的內(nèi)存塊的所有權(quán)傳給了auto_ptr p1, p1p的copy(注:這里從Fun函數(shù)的定義式看出,函數(shù)參數(shù)時(shí)值傳遞,所以把p的值拷進(jìn)了函數(shù)中),這時(shí)p1就擁有了之前p擁有的內(nèi)存塊。目前為止,一切安好?,F(xiàn)在Fun函數(shù)執(zhí)行完了,p1離開(kāi)了作用域,所以p1關(guān)聯(lián)的內(nèi)存塊也就釋放了。那么p呢?p什么都沒(méi)了,這就是crash的原因了,下一行代碼還試圖訪問(wèn)p,好像p還擁有什么資源似的。

Issue2:

還有另外一個(gè)缺點(diǎn)。auto_ptr不能指向一組對(duì)象,就是說(shuō)它不能和操作符new[]一起使用。

//***************************************************************
void main( )
{
 std::auto_ptr<Test> p(new Test[5]);
}

上面的代碼將產(chǎn)生一個(gè)運(yùn)行時(shí)錯(cuò)誤。因?yàn)楫?dāng)auto_ptr離開(kāi)作用域時(shí),delete被默認(rèn)用來(lái)釋放關(guān)聯(lián)的內(nèi)存空間。當(dāng)auto_ptr只指向一個(gè)對(duì)象時(shí),這當(dāng)然是沒(méi)問(wèn)題的,但是在上面的代碼里,我們?cè)诙牙飫?chuàng)建了一組對(duì)象,應(yīng)該使用delete[]來(lái)釋放,而不是delete.

Issue3:

auto_ptr不能和標(biāo)準(zhǔn)容器(vector,list,map....)一起使用。

因?yàn)?code>auto_ptr容易產(chǎn)生錯(cuò)誤,所以它也將被廢棄了。C++11提供了一組新的智能指針,每一個(gè)都各有用武之地。

  • shared_ptr
  • unique_ptr
  • weak_ptr

shared_ptr###

好吧,準(zhǔn)備享受真正的智能。第一種智能指針是shared_ptr,它有一個(gè)叫做共享所有權(quán)(sharedownership)的概念。shared_ptr的目標(biāo)非常簡(jiǎn)單:多個(gè)指針可以同時(shí)指向一個(gè)對(duì)象,當(dāng)最后一個(gè)shared_ptr離開(kāi)作用域時(shí),內(nèi)存才會(huì)自動(dòng)釋放。

創(chuàng)建:

void main( )
{
 shared_ptr<int> sptr1( new int );
}

使用make_shared宏來(lái)加速創(chuàng)建的過(guò)程。因?yàn)?code>shared_ptr主動(dòng)分配內(nèi)存并且保存引用計(jì)數(shù)(reference count),make_shared 以一種更有效率的方法來(lái)實(shí)現(xiàn)創(chuàng)建工作。

void main( )
{
 shared_ptr<int> sptr1 = make_shared<int>(100);
}

上面的代碼創(chuàng)建了一個(gè)shared_ptr,指向一塊內(nèi)存,該內(nèi)存包含一個(gè)整數(shù)100,以及引用計(jì)數(shù)1.如果通過(guò)sptr1再創(chuàng)建一個(gè)shared_ptr,引用計(jì)數(shù)就會(huì)變成2. 該計(jì)數(shù)被稱為強(qiáng)引用(strong reference),除此之外,shared_ptr還有另外一種引用計(jì)數(shù)叫做弱引用(weak reference),后面將介紹。

通過(guò)調(diào)用use_count()可以得到引用計(jì)數(shù), 據(jù)此你能找到shared_ptr的數(shù)量。當(dāng)debug的時(shí)候,可以通過(guò)觀察shared_ptrstrong_ref的值得到引用計(jì)數(shù)。

reference count

析構(gòu)

shared_ptr默認(rèn)調(diào)用delete釋放關(guān)聯(lián)的資源。如果用戶采用一個(gè)不一樣的析構(gòu)策略時(shí),他可以自由指定構(gòu)造這個(gè)shared_ptr的策略。下面的例子是一個(gè)由于采用默認(rèn)析構(gòu)策略導(dǎo)致的問(wèn)題:

class Test
{
public:
 Test(int a = 0 ) : m_a(a)
 {
 }
 ~Test( )
 {
  cout<<"Calling destructor"<<endl;
 }
public:
         int m_a;
};
void main( )
{
 shared_ptr<Test> sptr1( new Test[5] );
}

在此場(chǎng)景下,shared_ptr指向一組對(duì)象,但是當(dāng)離開(kāi)作用域時(shí),默認(rèn)的析構(gòu)函數(shù)調(diào)用delete釋放資源。實(shí)際上,我們應(yīng)該調(diào)用delete[]來(lái)銷毀這個(gè)數(shù)組。用戶可以通過(guò)調(diào)用一個(gè)函數(shù),例如一個(gè)lamda表達(dá)式,來(lái)指定一個(gè)通用的釋放步驟。

void main( )
{
 shared_ptr<Test> sptr1( new Test[5], 
        [ ](Test* p) { delete[ ] p; } );
}

通過(guò)指定delete[]來(lái)析構(gòu),上面的代碼可以完美運(yùn)行。

接口
就像一個(gè)普通指針一樣,shared_ptr也提供解引用操作符*,->。除此之外,它還提供了一些更重要的接口:

  • get(): 獲取shared_ptr綁定的資源.
  • reset(): 釋放關(guān)聯(lián)內(nèi)存塊的所有權(quán),如果是最后一個(gè)指向該資源的shared_ptr,就釋放這塊內(nèi)存。
  • unique: 判斷是否是唯一指向當(dāng)前內(nèi)存的shared_ptr.
  • operator bool : 判斷當(dāng)前的shared_ptr是否指向一個(gè)內(nèi)存塊,可以用if 表達(dá)式判斷。

OK,上面是所有關(guān)于shared_ptr的描述,但是shared_ptr也有一些問(wèn)題:
Issues:

void main( )
{
 shared_ptr<int> sptr1( new int );
 shared_ptr<int> sptr2 = sptr1;
 shared_ptr<int> sptr3;
 sptr3 =sptr1

Issues:
下表是上面代碼中引用計(jì)數(shù)變化情況:

引用計(jì)數(shù)變化

所有的shared_ptrs擁有相同的引用計(jì)數(shù),屬于相同的組。上述代碼工作良好,讓我們看另外一組例子。

void main( )
{
 int* p = new int;
 shared_ptr<int> sptr1( p);
 shared_ptr<int> sptr2( p );
}

上述代碼會(huì)產(chǎn)生一個(gè)錯(cuò)誤,因?yàn)閮蓚€(gè)來(lái)自不同組的shared_ptr指向同一個(gè)資源。下表給你關(guān)于錯(cuò)誤原因的圖景:

引用計(jì)數(shù)

避免這個(gè)問(wèn)題,盡量不要從一個(gè)裸指針(naked pointer)創(chuàng)建shared_ptr.

class B;
class A
{
public:
 A(  ) : m_sptrB(nullptr) { };
 ~A( )
 {
  cout<<" A is destroyed"<<endl;
 }
 shared_ptr<B> m_sptrB;
};
class B
{
public:
 B(  ) : m_sptrA(nullptr) { };
 ~B( )
 {
  cout<<" B is destroyed"<<endl;
 }
 shared_ptr<A> m_sptrA;
};
//***********************************************************
void main( )
{
 shared_ptr<B> sptrB( new B );
 shared_ptr<A> sptrA( new A );
 sptrB->m_sptrA = sptrA;
 sptrA->m_sptrB = sptrB;
}

上面的代碼產(chǎn)生了一個(gè)循環(huán)引用.A對(duì)B有一個(gè)shared_ptr, B對(duì)A也有一個(gè)shared_ptr ,與sptrAsptrB關(guān)聯(lián)的資源都沒(méi)有被釋放,參考下表:

sptrA&sptrB

當(dāng)sptrAsptrB離開(kāi)作用域時(shí),它們的引用計(jì)數(shù)都只減少到1,所以它們指向的資源并沒(méi)有釋放!?。。?!

  1. 如果幾個(gè)shared_ptrs指向的內(nèi)存塊屬于不同組,將產(chǎn)生錯(cuò)誤。
  2. 如果從一個(gè)普通指針創(chuàng)建一個(gè)shared_ptr還會(huì)引發(fā)另外一個(gè)問(wèn)題。在上面的代碼中,考慮到只有一個(gè)shared_ptr是由p創(chuàng)建的,代碼可以好好工作。萬(wàn)一程序員在智能指針作用域結(jié)束之前刪除了普通指針p。天啦嚕!?。∮质且粋€(gè)crash。
  3. 循環(huán)引用:如果共享智能指針卷入了循環(huán)引用,資源都不會(huì)正常釋放。

為了解決循環(huán)引用,C++提供了另外一種智能指針:weak_ptr

Weak_Ptr###

weak_ptr 擁有共享語(yǔ)義(sharing semantics)和不包含語(yǔ)義(not owning semantics)。這意味著,weak_ptr可以共享shared_ptr持有的資源。所以可以從一個(gè)包含資源的shared_ptr創(chuàng)建weak_ptr。

weak_ptr不支持普通指針包含的*,->操作。它并不包含資源所以也不允許程序員操作資源。既然如此,我們?nèi)绾问褂?code>weak_ptr呢?

答案是從weak_ptr中創(chuàng)建shared_ptr然后再使用它。通過(guò)增加強(qiáng)引用計(jì)數(shù),當(dāng)使用時(shí)可以確保資源不會(huì)被銷毀。當(dāng)引用計(jì)數(shù)增加時(shí),可以肯定的是從weak_ptr中創(chuàng)建的shared_ptr引用計(jì)數(shù)至少為1.否則,當(dāng)你使用weak_ptr就可能發(fā)生如下問(wèn)題:當(dāng)shared_ptr離開(kāi)作用域時(shí),其擁有的資源會(huì)釋放,從而導(dǎo)致了混亂。

創(chuàng)建

可以以shared_ptr作為參數(shù)構(gòu)造weak_ptr.從shared_ptr創(chuàng)建一個(gè)weak_ptr增加了共享指針的弱引用計(jì)數(shù)(weak reference),意味著shared_ptr與其它的指針共享著它所擁有的資源。但是當(dāng)shared_ptr離開(kāi)作用域時(shí),這個(gè)計(jì)數(shù)不作為是否釋放資源的依據(jù)。換句話說(shuō),就是除非強(qiáng)引用計(jì)數(shù)變?yōu)?code>0,才會(huì)釋放掉指針指向的資源,在這里,弱引用計(jì)數(shù)(weak reference)不起作用。

void main( )
{
 shared_ptr<Test> sptr( new Test );
 weak_ptr<Test> wptr( sptr );
 weak_ptr<Test> wptr1 = wptr;
}

可以從下圖觀察shared_ptrweak_ptr的引用計(jì)數(shù):

shared_ptr 和weak_ptr變化

將一個(gè)weak_ptr賦給另一個(gè)weak_ptr會(huì)增加弱引用計(jì)數(shù)(weak reference count)。

所以,當(dāng)shared_ptr離開(kāi)作用域時(shí),其內(nèi)的資源釋放了,這時(shí)候指向該shared_ptrweak_ptr發(fā)生了什么?weak_ptr過(guò)期了(expired)。

如何判斷weak_ptr是否指向有效資源,有兩種方法:

  1. 調(diào)用use-count()去獲取引用計(jì)數(shù),該方法只返回強(qiáng)引用計(jì)數(shù),并不返回弱引用計(jì)數(shù)。
  2. 調(diào)用expired()方法。比調(diào)用use_count()方法速度更快。

weak_ptr調(diào)用lock()可以得到shared_ptr或者直接將weak_ptr轉(zhuǎn)型為shared_ptr

void main( )
{
 shared_ptr<Test> sptr( new Test );
 weak_ptr<Test> wptr( sptr );
 shared_ptr<Test> sptr2 = wptr.lock( );
}

如之前所述,從weak_ptr中獲取shared_ptr增加強(qiáng)引用計(jì)數(shù)。

現(xiàn)在讓我們見(jiàn)識(shí)一下weak_ptr如何解決循環(huán)引用問(wèn)題:

class B;
class A
{
public:
 A(  ) : m_a(5)  { };
 ~A( )
 {
  cout<<" A is destroyed"<<endl;
 }
 void PrintSpB( );
 weak_ptr<B> m_sptrB;
 int m_a;
};
class B
{
public:
 B(  ) : m_b(10) { };
 ~B( )
 {
  cout<<" B is destroyed"<<endl;
 }
 weak_ptr<A> m_sptrA;
 int m_b;
};

void A::PrintSpB( )
{
 if( !m_sptrB.expired() )
 {  
  cout<< m_sptrB.lock( )->m_b<<endl;
 }
}

void main( )
{
 shared_ptr<B> sptrB( new B );
 shared_ptr<A> sptrA( new A );
 sptrB->m_sptrA = sptrA;
 sptrA->m_sptrB = sptrB;
 sptrA->PrintSpB( ); 
}

引用計(jì)數(shù)

unique_ptr###

unique_ptr也是對(duì)auto_ptr的替換。unique_ptr遵循著獨(dú)占語(yǔ)義。在任何時(shí)間點(diǎn),資源只能唯一地被一個(gè)unique_ptr占有。當(dāng)unique_ptr離開(kāi)作用域,所包含的資源被釋放。如果資源被其它資源重寫了,之前擁有的資源將被釋放。所以它保證了他所關(guān)聯(lián)的資源總是能被釋放。

創(chuàng)建
unique_ptr的創(chuàng)建方法和shared_ptr一樣,除非創(chuàng)建一個(gè)指向數(shù)組類型的unique_ptr。

unique_ptr<int> uptr( new int );

unique_ptr提供了創(chuàng)建數(shù)組對(duì)象的特殊方法,當(dāng)指針離開(kāi)作用域時(shí),調(diào)用delete[]代替delete。當(dāng)創(chuàng)建unique_ptr時(shí),這一組對(duì)象被視作模板參數(shù)的部分。這樣,程序員就不需要再提供一個(gè)指定的析構(gòu)方法,如下:

unique_ptr<int[ ]> uptr( new int[5] );

當(dāng)把unique_ptr賦給另外一個(gè)對(duì)象時(shí),資源的所有權(quán)就會(huì)被轉(zhuǎn)移。

記住unique_ptr不提供復(fù)制語(yǔ)義(拷貝賦值和拷貝構(gòu)造都不可以),只支持移動(dòng)語(yǔ)義(move semantics).

在上面的例子里,如果upt3upt5已經(jīng)擁有了資源,只有當(dāng)擁有新資源時(shí),之前的資源才會(huì)釋放。

接口

unique_ptr提供的接口和傳統(tǒng)指針差不多,但是不支持指針運(yùn)算。

unique_ptr提供一個(gè)release()的方法,釋放所有權(quán)。releasereset的區(qū)別在于,release僅僅釋放所有權(quán)但不釋放資源,reset也釋放資源。

使用哪一個(gè)?##

完全取決于你想要如何擁有一個(gè)資源,如果需要共享資源使用shared_ptr,如果獨(dú)占使用資源就使用unique_ptr.

除此之外,shared_ptrunique_ptr更加重,因?yàn)樗€需要分配空間做其它的事,比如存儲(chǔ)強(qiáng)引用計(jì)數(shù),弱引用計(jì)數(shù)。而unique_ptr不需要這些,它只需要獨(dú)占著保存資源對(duì)象。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 導(dǎo)讀## 最近在補(bǔ)看《C++ Primer Plus》第六版,這的確是本好書,其中關(guān)于智能指針的章節(jié)解析的非常清晰...
    小敏紙閱讀 2,079評(píng)論 1 12
  • C++智能指針 原文鏈接:http://blog.csdn.net/xiaohu2022/article/deta...
    小白將閱讀 6,994評(píng)論 2 21
  • 1. 什么是智能指針? 智能指針是行為類似于指針的類對(duì)象,但這種對(duì)象還有其他功能。 2. 為什么設(shè)計(jì)智能指針? 引...
    MinoyJet閱讀 707評(píng)論 0 1
  • 1. 簡(jiǎn)介 C++ 11 里面的智能指針 2. 為什么要用智能智能? 因?yàn)闀?huì)出現(xiàn)內(nèi)存泄漏的情況,即用new 申請(qǐng)了...
    DayDayUpppppp閱讀 1,177評(píng)論 1 8
  • 強(qiáng)類型枚舉 枚舉:分門別類與數(shù)值的名字 枚舉類型是C及C++中一個(gè)基本的內(nèi)置類型,不過(guò)也是一個(gè)有點(diǎn)"奇怪"的類型。...
    認(rèn)真學(xué)計(jì)算機(jī)閱讀 2,841評(píng)論 0 3

友情鏈接更多精彩內(nèi)容