C++智慧指標shared_ptr講解與使用
手動管理的弊端
在簡單的程式中,我們不大可能忘記釋放 new 出來的指標,但是隨著程式規模的增大,我們忘了 delete 的概率也隨之增大。在 C++ 中 new 出來的指標,賦值意味著引用的傳遞,當賦值運算子同時展現出“值拷貝”和“引用傳遞”兩種截然不同的語義時,就很容易導致“記憶體洩漏”。
手動管理記憶體帶來的更嚴重的問題是,記憶體究竟要由誰來分配和釋放呢?指標的賦值將同一物件的引用散播到程式各處,但是該物件的釋放卻只能發生一次。當在程式碼中用完了一個資源指標,該不該釋放 delete 掉它?這個資源極有可能同時被多個物件擁有著,而這些物件中的任何一個都有可能在之後使用該資源,其餘指向這個物件的指標就變成了“野指標”;那如果不 delete 呢?也許你就是這個資源指標的唯一使用者,如果你用完不 delete,記憶體就洩漏了。
資源的擁有者是系統,當我們需要時便向系統申請資源,當我們不需要時就讓系統自己收回去(Garbage Collection)。當我們自己處理的時候,就容易出現各種各樣的問題。
C++中的智慧指標
自C++11起,C++標準提供兩大型別的智慧指標:
1. Class shared_ptr實現共享式擁有(shared ownership)概念。多個智慧指標可以指向相同物件,該物件和其相關資源會在“最後一個引用(reference)被銷燬”時候釋放。為了在結構複雜的情境中執行上述工作,標準庫提供了weak_ptr、bad_weak_ptr和enable_shared_from_this等輔助類。
2. Class unique_ptr實現獨佔式擁有(exclusive ownership)或嚴格擁有(strict ownership)概念 ,保證同一時間內只有一個智慧指標可以指向該物件。它對於避免資源洩露(resourece leak)——例如“以new建立物件後因為發生異常而忘記呼叫delete”——特別有用。
注:C++98中的Class auto_ptr在C++11中已不再建議使用。
share_ptr
智慧指標是(幾乎總是)模板類,shared_ptr 同樣是模板類,所以在建立 shared_ptr 時需要指定其指向的型別。shared_ptr 負責在不使用例項時釋放由它管理的物件,同時它可以自由的共享它指向的物件。
shared_ptr 使用經典的 “引用計數” 的方法來管理物件資源。引用計數指的是,所有管理同一個裸指標( raw pointer )的 shared_ptr,都共享一個引用計數器,每當一個 shared_ptr 被賦值(或拷貝構造)給其它 shared_ptr 時,這個共享的引用計數器就加1,當一個 shared_ptr 析構或者被用於管理其它裸指標時,這個引用計數器就減1,如果此時發現引用計數器為0,那麼說明它是管理這個指標的最後一個 shared_ptr 了,於是我們釋放指標指向的資源。
在底層實現中,這個引用計數器儲存在某個內部型別裡(這個型別中還包含了 deleter,它控制了指標的釋放策略,預設情況下就是普通的delete操作),而這個內部型別物件在 shared_ptr 第一次構造時以指標的形式儲存在 shared_ptr 中(所以一個智慧指標的析構會影響到其他指向同一位置的智慧指標)。shared_ptr 過載了賦值運算子,在賦值和拷貝構造另一個 shared_ptr 時,這個指標被另一個 shared_ptr 共享。在引用計數歸零時,這個內部型別指標與 shared_ptr 管理的資源一起被釋放。此外,為了保證執行緒安全性,引用計數器的加1,減1操作都是 原子操作,它保證 shared_ptr 由多個執行緒共享時不會爆掉。
對於 shared_ptr 在拷貝和賦值時的行為,《C++Primer第五版》中有詳細的描述:
每個 shared_ptr 都有一個關聯的計數值,通常稱為引用計數。無論何時我們拷貝一個 shared_ptr,計數器都會遞增。 例如,當用一個 shared_ptr 初始化另一個 shred_ptr,或將它當做引數傳遞給一個函式以及作為函式的返回值時,它所關聯的計數器就會遞增。當我們給 shared_ptr 賦予一個新值或是 shared_ptr 被銷燬(例如一個區域性的 shared_ptr 離開其作用域)時,計數器就會遞減。一旦一個 shared_ptr 的計數器變為0,它就會自動釋放自己所管理的物件。
下面看一個常見用法,包括: 1. 建立 shared_ptr 例項 2. 訪問所指物件 3. 拷貝和賦值操作 4. 檢查引用計數。
#include <iostream>
#include <string>
#include <tr1/memory>
using namespace std;
using namespace std::tr1;
class Test
{
public:
Test(string name)
{
name_ = name;
cout << this->name_ << " constructor" << endl;
}
~Test()
{
cout << this->name_ << " destructor" << endl;
}
string name_;
};
int main()
{
/* 類物件 原生指標構造 */
shared_ptr<Test> pStr1(new Test("object"));
cout << (*pStr1).name_ << endl;
/* use_count()檢查引用計數 */
cout << "pStr1 引用計數:" << pStr1.use_count() << endl;
shared_ptr<Test> pStr2 = pStr1;
cout << (*pStr2).name_ << endl;
cout << "pStr1 引用計數:" << pStr1.use_count() << endl;
cout << "pStr2 引用計數:" << pStr2.use_count() << endl;
/* 先new 一個物件,把原始指標傳遞給shared_ptr的建構函式 */
int *pInt1 = new int(11);
shared_ptr<int> pInt2(pInt1);
/* unique()來檢查某個shared_ptr 是否是原始指標唯一擁有者 */
cout << pInt2.unique() << endl; //true 1
/* 用一個shared_ptr物件來初始化另一個shared_ptr例項 */
shared_ptr<int> pInt3(pInt2);
cout << pInt2.unique() << endl; //false 0
cout << pInt3.use_count() << endl;
cout << pInt2.use_count() << endl;
return 0;
}
輸出結果如下:
錯誤用法一:迴圈引用
迴圈引用可以說是引用計數策略最大的缺點,“迴圈引用”簡單來說就是:兩個物件互相使用一個 shared_ptr 成員變數指向對方(你中有我,我中有你)。突然想到一個問題:垃圾回收器是如何處理迴圈引用的? 下面看一個例子:
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class Children;
class Parent
{
public:
~Parent()
{
cout << "Parent destructor" << endl;
}
shared_ptr<Children> children;
};
class Children
{
public:
~Children()
{
cout << "Children destructor" << endl;
}
shared_ptr<Parent> parent;
};
void Test()
{
shared_ptr<Parent> pParent(new Parent());
shared_ptr<Children> pChildren(new Children());
if(pParent && pChildren)
{
pParent -> children = pChildren;
pChildren -> parent = pParent;
}
cout << "pParent use_count: " << pParent.use_count() << endl;
cout << "pChildren use_count: " << pChildren.use_count() << endl;
}
int main()
{
Test();
return 0;
}
輸出結果如下:
退出之前,它們的 use_count() 都為2,退出了 Test() 後,由於 pParent 和 pChildren 物件互相引用,它們的引用計數都是 1,不能自動釋放(可以看到沒有呼叫解構函式),並且此時這兩個物件再無法訪問到。這就引起了c++中那臭名昭著的“記憶體洩漏”。
那麼如何解除迴圈引用呢?
weak_ptr
使用weak_ptr 來打破迴圈引用,它與一個 shared_ptr 繫結,但卻不參與引用計數的計算,不論是否有 weak_ptr 指向,一旦最後一個指向物件的 shared_ptr 被銷燬,物件就會被釋放。weak_ptr 像是 shared_ptr 的一個助手。同時,在需要時,它還能搖身一變,生成一個與它繫結的 shared_ptr 共享引用計數的新 shared_ptr。
總而言之,weak_ptr 的作用就是:在需要時變出一個 shared_ptr,在其他時候不干擾 shared_ptr 的引用計數。
它沒有過載 * 和 -> 運算子,因此不可以直接通過 weak_ptr 訪問物件,典型的用法是通過 lock() 成員函式來獲得 shared_ptr,進而使用物件。 下面是 weak_ptr 的一般用法:
std::shared_ptr<int> sh = std::make_shared<int>();
// 用一個shared_ptr初始化
std::weak_ptr<int> w(sh);
// 變出 shared_ptr
std::shared_ptr<int> another = w.lock();
// 判斷weak_ptr所觀察的shared_ptr的資源是否已經釋放
bool isDeleted = w.expired();
我們看看它如何來解決上面的迴圈引用。
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class Children;
class Parent
{
public:
~Parent()
{
cout << "Parent destructor" << endl;
}
weak_ptr<Children> children; //注意這裡
};
class Children
{
public:
~Children()
{
cout << "Children destructor" << endl;
}
weak_ptr<Parent> parent; //注意這裡
};
void Test()
{
shared_ptr<Parent> pParent(new Parent());
shared_ptr<Children> pChildren(new Children());
if(pParent && pChildren)
{
pParent -> children = pChildren;
pChildren -> parent = pParent;
}
// 看一下各自的引用計數
cout << "pParent use_count: " << pParent.use_count() << endl;
cout << "pChildren use_count: " << pChildren.use_count() << endl;
}
int main()
{
Test();
return 0;
}
輸出結果:
可以看到各自的引用計數分別為1,而且函式執行完畢後,各自指向的物件得到析構,這樣來解除了上面的迴圈引用。
std::shared_ptr大概總結有以下幾點:
(1) 智慧指標主要的用途就是方便資源的管理,自動釋放沒有指標引用的資源。
(2) 使用引用計數來標識是否有多餘指標指向該資源。(注意,shart_ptr本身指標會佔1個引用)
(3) 在賦值操作中, 原來資源的引用計數會減一,新指向的資源引用計數會加一。
std::shared_ptr<Test> p1(new Test);
std::shared_ptr<Test> p2(new Test);
p1 = p2;
(4) 引用計數加一/減一操作是原子操作,所以執行緒安全的。
(5) make_shared要優於使用new,make_shared可以一次將需要記憶體分配好。
std::shared_ptr<Test> p = std::make_shared<Test>();
std::shared_ptr<Test> p(new Test);
(6) std::shared_ptr的大小是原始指標的兩倍,因為它的內部有一個原始指標指向資源,同時有個指標指向引用計數。
(7) 引用計數是分配在動態分配的,std::shared_ptr支援拷貝,新的指標獲可以獲取前引用計數個數。