原作者: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è)RAII(Resource 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, p1是p的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_ptr中strong_ref的值得到引用計(jì)數(shù)。
析構(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ù)變化情況:
所有的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ò)誤原因的圖景:
避免這個(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 ,與sptrA和sptrB關(guān)聯(lián)的資源都沒(méi)有被釋放,參考下表:
當(dāng)
sptrA和sptrB離開(kāi)作用域時(shí),它們的引用計(jì)數(shù)都只減少到1,所以它們指向的資源并沒(méi)有釋放!?。。?!
- 如果幾個(gè)
shared_ptrs指向的內(nèi)存塊屬于不同組,將產(chǎn)生錯(cuò)誤。 - 如果從一個(gè)普通指針創(chuàng)建一個(gè)
shared_ptr還會(huì)引發(fā)另外一個(gè)問(wèn)題。在上面的代碼中,考慮到只有一個(gè)shared_ptr是由p創(chuàng)建的,代碼可以好好工作。萬(wàn)一程序員在智能指針作用域結(jié)束之前刪除了普通指針p。天啦嚕!?。∮质且粋€(gè)crash。 - 循環(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_ptr和weak_ptr的引用計(jì)數(shù):
將一個(gè)weak_ptr賦給另一個(gè)weak_ptr會(huì)增加弱引用計(jì)數(shù)(weak reference count)。
所以,當(dāng)shared_ptr離開(kāi)作用域時(shí),其內(nèi)的資源釋放了,這時(shí)候指向該shared_ptr的weak_ptr發(fā)生了什么?weak_ptr過(guò)期了(expired)。
如何判斷weak_ptr是否指向有效資源,有兩種方法:
- 調(diào)用
use-count()去獲取引用計(jì)數(shù),該方法只返回強(qiáng)引用計(jì)數(shù),并不返回弱引用計(jì)數(shù)。 - 調(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( );
}
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).
在上面的例子里,如果upt3和upt5已經(jīng)擁有了資源,只有當(dāng)擁有新資源時(shí),之前的資源才會(huì)釋放。
接口
unique_ptr提供的接口和傳統(tǒng)指針差不多,但是不支持指針運(yùn)算。
unique_ptr提供一個(gè)release()的方法,釋放所有權(quán)。release和reset的區(qū)別在于,release僅僅釋放所有權(quán)但不釋放資源,reset也釋放資源。
使用哪一個(gè)?##
完全取決于你想要如何擁有一個(gè)資源,如果需要共享資源使用shared_ptr,如果獨(dú)占使用資源就使用unique_ptr.
除此之外,shared_ptr比unique_ptr更加重,因?yàn)樗€需要分配空間做其它的事,比如存儲(chǔ)強(qiáng)引用計(jì)數(shù),弱引用計(jì)數(shù)。而unique_ptr不需要這些,它只需要獨(dú)占著保存資源對(duì)象。