1. 程式人生 > >C++:智慧指標之shared_ptr

C++:智慧指標之shared_ptr

1.智慧指標

C++中用new來動態分配記憶體,delete手動釋放記憶體來達到動態管理記憶體的目的。因為保證在正確的時間釋放記憶體是非常困難的,忘記釋放記憶體就會產生記憶體洩露。

為了更安全、便捷的使用動態記憶體,C++11標準庫提供了新的智慧指標類來管理動態記憶體。智慧指標在行為上和普通的指標是一樣的,只不過它可以保證記憶體在適當的時候自動釋放。
新的標準庫提供了兩種智慧指標(在標頭檔案<memory>中),區別在於管理底層指標的方式。

  • shared_ptr允許多個指標指向同一個物件。
  • unique_ptr“獨佔”指向的物件。

本文僅介紹shared_ptr的相關用法。

2.建立shared_ptr智慧指標

類似vector,智慧指標也是一種模板,因此建立智慧指標時,我們需要提供額外的資訊表明指標指向的型別。
shared_ptr<int> p1;//建立一個空指標p1,可以指向int型別
shared_ptr<list<int>> p2;//建立一個空指標p2,可以指向list<int>型別
這僅僅是建立了一個智慧指標,並沒有給它分配記憶體。
通常有兩種方式給shared_ptr分配記憶體

make_shared函式分配記憶體

最安全的分配和使用動態內部的方式是呼叫名為make_shared的標準庫函式。此函式在動態記憶體中分配一個物件並初始化它,返回指向此物件的shared_ptr。

shared_ptr<int> p1 = make_shared<int>(42);
//建立指向int的shard_ptr並且用42初始化
shared_ptr<string> p2 = make_shared<string>(10, '9');
//建立一個指向string的shared_ptr並且呼叫string的(n,char)建構函式初始化
shared_ptr<my_class> p3 = make_shared<my_class>();
//建立指向my_class的shared_ptr並且呼叫my_class的預設建構函式初始化

類似於容器操作的emplace,make_shared函式用其引數來構造給定型別的物件。所以說如果是構造一個類的shared_ptr,一定要注意初始化時必須和某個建構函式匹配

前邊提到過,shared_ptr允許多個指標指向同一個物件。所以我們可以認為每一個shared_ptr都有一個和它關聯的計數器,稱為引用計數器。當滿足下述條件時,引用計數器遞增。

  • 初始化shared_ptr: shared_ptr<int> p1 = make_shared<int>(42);此時p1指向的物件只有p1一個引用者,引用計數為1

  • 拷貝shared_ptr:shared_ptr<int> q(p);此時q和p指向同一個物件,該物件的引用計數為2

  • 將shared_ptr當做一個函式的引數傳遞:

shared_ptr<int> p1 = make_shared<int>(42);
printCount(p1);
void printCount(shared_ptr<int> p)
{
    cout << p.use_count() << endl;//返回p指向物件的引用計數
}

輸出結果是2,因為p1被當做了引數傳遞

  • 將shared_ptr用作函式的返回值
shared_ptr<vector<int>> getVector()
{
    return make_shared<vector<int>>();
}
shared_ptr<vector<int>> p = getVector();
cout << p.use_count() << endl;

輸出結果是1,雖然初始化的時候沒有顯式的用make_shared函式給p分配記憶體,但是呼叫了getVector()函式,返回了一個shard_ptr,這樣p指向的物件引用計數器增加。

當一個物件的引用計數減為0後,shared_ptr會自動銷燬物件,釋放記憶體。

當以下兩種情況發生時,shared_ptr的引用計數會遞減

  • 給shared_ptr賦予新值:
auto p1 = make_shared<int>(42);
p1 = p2;

給p1賦值使它指向p2指向的物件,會遞增p2指向的物件的引用計數,遞減p1指向物件的引用計數,此時p1指向物件的引用計數為0,被自動銷燬。

  • 區域性shared_ptr離開其作用域:
void printCount(shared_ptr<int> p)
{
    shared_ptr<int> x = make_shared<int>();
    cout << x.use_count() << endl;
}

x是區域性shared_ptr,當函式結束時,x指向的物件引用計數減為0,x指向的物件被銷燬,釋放佔用的記憶體。

shared_ptr與new結合使用建立智慧指標

如前所說,如果不初始化一個智慧指標,那麼它就會被初始化為一個空指標。除了用make_shared函式我們還可以用new返回指標初始化智慧指標。
例如shared_ptr<int> p(new int(42));
該語句實際上呼叫了智慧指標的建構函式,該建構函式的引數是普通指標。這種建構函式是顯式的,因此shared_ptr<int> p = new int(42);這種方式是錯誤的。
同樣的理由,一個返回shared_ptr的函式不能在其返回語句中隱式轉換一個普通指標。

shared_ptr<int> clone(int p)
{
    return new int(p);
    //錯誤,內建指標不能被隱式的轉換為shared_ptr
}

正確寫法應當是:

shared_ptr<int> clone(int p)
{
    return shared_ptr<int> (new int(p));
} 

還有,必須要強調的一點是:
預設情況下,一個用來初始化shared_ptr的普通指標必須指向動態分配的記憶體,這是因為智慧指標預設用delete釋放它關聯的記憶體
如果出於一些目的,必須用不指向動態分配記憶體的指標來初始化智慧指標,就必須自定義一種釋放記憶體的方式(可以理解為過載delete)。
詳細例子在3.shared_ptr支援的操作中介紹。

3.shared_ptr支援的操作

操作 解釋
shared_ptr< T > sp 建立空智慧指標,可以指向T型別的物件
sp.get() 返回sp中儲存的指標。PS:一定要小心使用,如果智慧指標釋放了物件,返回的指標指向的物件也將消失
sp.reset() 若sp是唯一指向該物件的shared_ptr,reset釋放該物件
sp.reset(p) sp不在指向原來它指向的物件,指向內建指標p指向的物件,這裡p是被new動態分配記憶體的
sp.reset(p,d) sp不在指向原來它指向的物件,指向內建指標p指向的物件,這裡p將會被可呼叫物件d釋放
sp.use_count() 返回sp指向物件的引用計數
sp.unique() 如果sp.use_count() == 1,返回true否則返回false
swap(sp1,sp2) 交換兩個智慧指標
shared_ptr< T > sp(p) sp管理內建指標p,p必須是被new動態分配記憶體的,而且能轉換為T*型別
shared_ptr< T > sp(p,d) sp接管內建指標p指向物件的所有權,p必須能準換為T*型別,sp將使用可呼叫物件d代替delete
shared_ptr< T > sp1(sp2,d) sp1是shared_ptr sp2的拷貝,使用可呼叫物件d代替delete

上述操作表中,引入了一種新的做法,使用可呼叫物件d來釋放內建指標

這基於這樣的假設:
內建指標p指向的物件不是由new動態分配的,然而我們還是用了p初始化了sp(shared_ptr),此時就必須用“過載”delete函式,順利釋放p的記憶體。
這樣是很麻煩的,具體的例子可以看這裡:
C++ Primer 第五版 練習12.4

C++ Primer建議不要混合使用普通指標和智慧指標,更加推薦使用智慧指標。
為此,介紹幾個例子:

假如說存在函式:

void process(shared_ptr<int> ptr)
{
    //do something 
}//ptr離開作用域,被ptr被銷燬,但是它指向的記憶體不會被釋放

此函式的正確使用方法傳遞shared_ptr:

shared_ptr<int> ptr(new int(42));
//ptr指向物件引用計數為1
process(ptr);
//process中ptr指向的物件引用計數為2
int i = *ptr;
//正確,引用計數為2

典型的錯誤用法如下:

int * x(new int(1024));
process(x);
//錯誤,內建指標不能隱式的轉換為shared_ptr
process(shared_ptr<int> (x));
//正確,process中ptr.use_count() == 1,process結束時ptr指向的記憶體會被釋放!
int i = *x;
//未定義行為,x是空懸指標!

首先,語句shared_ptr<int>(x)相當於一個強制轉換,得到一個臨時變數shared_ptr,呼叫process時,相當於void process(shared_ptr<int> ptr(x)){},那麼當process結束的時候,ptr指向的物件的引用計數變為0,記憶體被釋放,也就是x指標指向的記憶體被釋放,x變為了一個空懸指標。

記住:當shared_ptr繫結到內建指標時。我們就把記憶體的管理責任教給了shared_ptr,不要再用內建指標訪問該記憶體。

出於近乎相同的理由,不要使用get返回的內建指標初始化其他的智慧指標!