原做者:Babu_Abdulsalam 本文翻譯自CodeProject,轉載請註明出處。程序員
儘管有另一篇文章說C++11
裏的智能指針了。近來,我聽到許多人談論C++
新標準,就是所謂的C++0x/C++11
。 我研究了一下C++11
的一些語言特性,發現確實它確實有一些巨大的改變。我將重點關注C++11
的智能指針部分。數組
普通指針(normal/raw/naked pointers)的問題?bash
讓咱們一個接一個的討論。函數
若是不恰當處理指針就會帶來許多問題,因此人們老是避免使用它。這也是許多新手程序員不喜歡指針的緣由。指針老是會扯上不少問題,例如指針所指向對象的生命週期,掛起引用(dangling references
)以及內存泄露。ui
若是一塊內存被多個指針引用,但其中的一個指針釋放且其他的指針並不知道,這樣的狀況下,就發生了掛起引用。而內存泄露,就如你知道的同樣,當從堆中申請了內存後不釋放回去,這時就會發生內存泄露。有人說,我寫了清晰而且帶有錯誤驗證的代碼,爲何我還要使用智能指針呢?一個程序員也問我:「嗨,下面是個人代碼,我從堆(heap
)中申請了一塊內存,使用完後,我又正確的把它歸還給了堆,那麼使用智能指針的必要在哪裏?」spa
void Foo( )
{
int* iPtr = new int[5];
//manipulate the memory block . . .
delete[ ] iPtr;
}
複製代碼
理想情況下,上面這段代碼確實可以工做的很好,內存也可以恰當的釋放回去。可是仔細思考一下實際的工做環境以及代碼執行條件。在內存分配和釋放的間隙,程序指令確實能作許多糟糕的事情,好比訪問無效的內存地址,除以0
,或者有另一個程序員在你的程序中修改了一個bug,他根據一個條件增長了一個過早的返回語句。翻譯
在以上全部狀況下,你的程序都走不到內存釋放的那部分。前兩種狀況下,程序拋出了異常,而第三種狀況,內存還沒釋放,程序就過早的return了。因此程序運行時,內存就已經泄露了。debug
解決以上全部問題的方法就是使用智能指針[若是它們足夠智能的話]。3d
什麼是智能指針?指針
智能指針是一個RAII
(Resource Acquisition is initialization
)類模型,用來動態的分配內存。它提供全部普通指針提供的接口,卻不多發生異常。在構造中,它分配內存,當離開做用域時,它會自動釋放已分配的內存。這樣的話,程序員就從手動管理動態內存的繁雜任務中解放出來了。
C++98提供了第一種智能指針:auto_ptr
讓咱們來見識一下auto_ptr
如何解決上述問題的吧。
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;
}
複製代碼
上述代碼會智能地釋放與指針綁定的內存。做用的過程是這樣的:咱們申請了一塊內存來放Test
對象,而且把他綁定到auto_ptr
p
上。因此當p
離開做用域時,它所指向的內存塊也會被自動釋放。
//***************************************************************
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;
}
}
複製代碼
上面的例子中,儘管異常被拋出,可是指針仍然正確地被釋放了。這是由於當異常拋出時,棧鬆綁(stack unwinding
),當try
塊中的全部對象destroy後,p
離開了該做用域,因此它綁定的內存也就釋放了。
Issue1:
目前爲止,auto_ptr
仍是足夠智能的,可是它仍是有一些根本性的破綻的。當把一個auto_ptr
賦給另一個auto_ptr
時,它的全部權(ownship)也轉移了。當我在函數間傳遞auto_ptr
時,這就是一個問題。話說,我在Foo()
中有一個auto_ptr
,而後在Foo()
中我把指針傳遞給了Fun()
函數,當Fun()
函數執行完畢時,指針的全部權不會再返還給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
的野指針行爲,上面的代碼致使程序崩潰。在這期間發生了這些細節,p
擁有一塊內存,當Fun
調用時, p
把關聯的內存塊的全部權傳給了auto_ptr p1
, p1
是p
的copy(注:這裏從Fun
函數的定義式看出,函數參數時值傳遞,因此把p
的值拷進了函數中),這時p1
就擁有了以前p
擁有的內存塊。目前爲止,一切安好。如今Fun
函數執行完了,p1
離開了做用域,因此p1
關聯的內存塊也就釋放了。那麼p
呢?p
什麼都沒了,這就是crash的緣由了,下一行代碼還試圖訪問p
,好像p
還擁有什麼資源似的。
Issue2:
還有另一個缺點。auto_ptr不能指向一組對象,就是說它不能和操做符new[]一塊兒使用。
//***************************************************************
void main( ) {
std::auto_ptr<Test> p(new Test[5]);
}
複製代碼
上面的代碼將產生一個運行時錯誤。由於當auto_ptr
離開做用域時,delete
被默認用來釋放關聯的內存空間。當auto_ptr
只指向一個對象時,這固然是沒問題的,可是在上面的代碼裏,咱們在堆裏建立了一組對象,應該使用delete[]
來釋放,而不是delete
.
Issue3:
auto_ptr
不能和標準容器(vector,list,map....)一塊兒使用。
由於auto_ptr
容易產生錯誤,因此它也將被廢棄了。C++11
提供了一組新的智能指針,每個都各有用武之地。
好吧,準備享受真正的智能。第一種智能指針是shared_ptr
,它有一個叫作共享全部權(sharedownership)的概念。shared_ptr
的目標很是簡單:多個指針能夠同時指向一個對象,當最後一個shared_ptr
離開做用域時,內存纔會自動釋放。
建立:
void main( )
{
shared_ptr<int> sptr1( new int );
}
複製代碼
使用make_shared
宏來加速建立的過程。由於shared_ptr
主動分配內存而且保存引用計數(reference count
),make_shared
以一種更有效率的方法來實現建立工做。
void main( )
{
shared_ptr<int> sptr1 = make_shared<int>(100);
}
複製代碼
上面的代碼建立了一個shared_ptr
,指向一塊內存,該內存包含一個整數100
,以及引用計數1
.若是經過sptr1
再建立一個shared_ptr
,引用計數就會變成2. 該計數被稱爲強引用(strong reference)
,除此以外,shared_ptr
還有另一種引用計數叫作弱引用(weak reference)
,後面將介紹。
經過調用use_count()
能夠獲得引用計數, 據此你能找到shared_ptr
的數量。當debug的時候,能夠經過觀察shared_ptr
中strong_ref
的值獲得引用計數。
析構
shared_ptr
默認調用delete
釋放關聯的資源。若是用戶採用一個不同的析構策略時,他能夠自由指定構造這個shared_ptr
的策略。下面的例子是一個因爲採用默認析構策略致使的問題:
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] );
}
複製代碼
在此場景下,shared_ptr
指向一組對象,可是當離開做用域時,默認的析構函數調用delete
釋放資源。實際上,咱們應該調用delete[]
來銷燬這個數組。用戶能夠經過調用一個函數,例如一個lamda
表達式,來指定一個通用的釋放步驟。
void main( )
{
shared_ptr<Test> sptr1( new Test[5],
[ ](Test* p) { delete[ ] p; } );
}
複製代碼
經過指定delete[]來析構,上面的代碼能夠完美運行。
接口 就像一個普通指針同樣,shared_ptr
也提供解引用操做符*
,->
。除此以外,它還提供了一些更重要的接口:
get()
: 獲取shared_ptr
綁定的資源.reset()
: 釋放關聯內存塊的全部權,若是是最後一個指向該資源的shared_ptr
,就釋放這塊內存。unique
: 判斷是不是惟一指向當前內存的shared_ptr
.operator bool
: 判斷當前的shared_ptr
是否指向一個內存塊,能夠用if 表達式判斷。OK,上面是全部關於shared_ptr
的描述,可是shared_ptr
也有一些問題: Issues:
void main( )
{
shared_ptr<int> sptr1( new int );
shared_ptr<int> sptr2 = sptr1;
shared_ptr<int> sptr3;
sptr3 =sptr1
複製代碼
Issues: 下表是上面代碼中引用計數變化狀況:
全部的shared_ptrs
擁有相同的引用計數,屬於相同的組。上述代碼工做良好,讓咱們看另一組例子。
void main( )
{
int* p = new int;
shared_ptr<int> sptr1( p);
shared_ptr<int> sptr2( p );
}
複製代碼
上述代碼會產生一個錯誤,由於兩個來自不一樣組的shared_ptr
指向同一個資源。下表給你關於錯誤緣由的圖景:
避免這個問題,儘可能不要從一個裸指針(naked pointer)
建立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;
}
複製代碼
上面的代碼產生了一個循環引用.A
對B
有一個shared_ptr
, B
對A
也有一個shared_ptr
,與sptrA
和sptrB
關聯的資源都沒有被釋放,參考下表:
sptrA
和
sptrB
離開做用域時,它們的引用計數都只減小到1,因此它們指向的資源並無釋放!!!!!
shared_ptrs
指向的內存塊屬於不一樣組,將產生錯誤。shared_ptr
還會引起另一個問題。在上面的代碼中,考慮到只有一個shared_ptr
是由p
建立的,代碼能夠好好工做。萬一程序員在智能指針做用域結束以前刪除了普通指針p
。天啦嚕!!!又是一個crash。爲了解決循環引用,C++
提供了另一種智能指針:weak_ptr
weak_ptr
擁有共享語義(sharing semantics
)和不包含語義(not owning semantics
)。這意味着,weak_ptr
能夠共享shared_ptr
持有的資源。因此能夠從一個包含資源的shared_ptr
建立weak_ptr
。
weak_ptr
不支持普通指針包含的*
,->
操做。它並不包含資源因此也不容許程序員操做資源。既然如此,咱們如何使用weak_ptr
呢?
答案是從weak_ptr
中建立shared_ptr
而後再使用它。經過增長強引用計數,當使用時能夠確保資源不會被銷燬。當引用計數增長時,能夠確定的是從weak_ptr
中建立的shared_ptr
引用計數至少爲1
.不然,當你使用weak_ptr
就可能發生以下問題:當shared_ptr
離開做用域時,其擁有的資源會釋放,從而致使了混亂。
建立
能夠以shared_ptr
做爲參數構造weak_ptr
.從shared_ptr
建立一個weak_ptr
增長了共享指針的弱引用計數(weak reference
),意味着shared_ptr
與其它的指針共享着它所擁有的資源。可是當shared_ptr
離開做用域時,這個計數不做爲是否釋放資源的依據。換句話說,就是除非強引用計數變爲0
,纔會釋放掉指針指向的資源,在這裏,弱引用計數(weak reference
)不起做用。
void main( )
{
shared_ptr<Test> sptr( new Test );
weak_ptr<Test> wptr( sptr );
weak_ptr<Test> wptr1 = wptr;
}
複製代碼
能夠從下圖觀察shared_ptr
和weak_ptr
的引用計數:
將一個weak_ptr
賦給另外一個weak_ptr
會增長弱引用計數(weak reference count
)。
因此,當shared_ptr
離開做用域時,其內的資源釋放了,這時候指向該shared_ptr
的weak_ptr
發生了什麼?weak_ptr
過時了(expired
)。
如何判斷weak_ptr
是否指向有效資源,有兩種方法:
use-count()
去獲取引用計數,該方法只返回強引用計數,並不返回弱引用計數。expired()
方法。比調用use_count()
方法速度更快。從weak_ptr
調用lock()
能夠獲得shared_ptr
或者直接將weak_ptr
轉型爲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
增長強引用計數。
如今讓咱們見識一下weak_ptr
如何解決循環引用問題:
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
也是對auto_ptr
的替換。unique_ptr
遵循着獨佔語義。在任什麼時候間點,資源只能惟一地被一個unique_ptr
佔有。當unique_ptr
離開做用域,所包含的資源被釋放。若是資源被其它資源重寫了,以前擁有的資源將被釋放。因此它保證了他所關聯的資源老是能被釋放。
建立 unique_ptr
的建立方法和shared_ptr
同樣,除非建立一個指向數組類型的unique_ptr
。
unique_ptr<int> uptr( new int );
複製代碼
unique_ptr
提供了建立數組對象的特殊方法,當指針離開做用域時,調用delete[]
代替delete
。當建立unique_ptr
時,這一組對象被視做模板參數的部分。這樣,程序員就不須要再提供一個指定的析構方法,以下:
unique_ptr<int[ ]> uptr( new int[5] );
複製代碼
當把unique_ptr
賦給另一個對象時,資源的全部權就會被轉移。
記住unique_ptr
不提供複製語義(拷貝賦值和拷貝構造都不能夠),只支持移動語義(move semantics
).
在上面的例子裏,若是upt3
和upt5
已經擁有了資源,只有當擁有新資源時,以前的資源纔會釋放。
接口
unique_ptr提供的接口和傳統指針差很少,可是不支持指針運算。
unique_ptr
提供一個release()
的方法,釋放全部權。release
和reset
的區別在於,release
僅僅釋放全部權但不釋放資源,reset
也釋放資源。
徹底取決於你想要如何擁有一個資源,若是須要共享資源使用shared_ptr
,若是獨佔使用資源就使用unique_ptr
.
除此以外,shared_ptr
比unique_ptr
更加劇,由於他還須要分配空間作其它的事,好比存儲強引用計數,弱引用計數。而unique_ptr
不須要這些,它只須要獨佔着保存資源對象。