Shared_ptr詳解

shared_ptr在boost庫中已經有多年了,C++11又爲其正名,把他引入了STL庫,放到了std的下面,可見其頗有用武之地;但是shared_ptr是萬能的嗎?有沒有什麼樣的問題呢?本文並不說明shared_ptr的設計原理,也不是爲了說明如何使用,只說一下在使用過程中的幾點注意事項。

用法


shared_ptr<int> sp(new int(10));                //一個指向整數的shared_ptr  
assert(sp.unique());                            //現在shared_ptr是指針的唯一持有者   
shared_ptr<int> sp2 = sp;                       //第二個shared_ptr,拷貝構造函數   
assert(sp == sp2 && sp.use_count() == 2);       //兩個shared_ptr相等,指向同一個對象,引用計數爲2  
*sp2 = 100;                                     //使用解引用操作符修改被指對象  
assert(*sp == 100);                             //另一個shared_ptr也同時被修改   
sp.reset();                                     //停止shared_ptr的使用  
assert(!sp);                                    //sp不再持有任何指針(空指針)

shared_ptr的線程安全性

shared_ptr本身不是100%線程安全的,它的引用計數本身是安全且無鎖的,但對象的讀寫則不是,因爲shared_ptr有兩個數據成員,讀寫操作不能原子化。根據文檔,shared_ptr的線程安全級別和內建類型、標準容器、string一樣,即(參考智能指針線程相關):

  • 一個shared_ptr 實體可被多個線程同時讀取;
  • 兩個shared_ptr 實體可以被兩個線程同時寫入,“析構”算寫操作;
  • 如果要從多個線程讀寫同一個shared_ptr對象,那麼需要加鎖;

智能指針是萬能良藥?

智能指針爲解決資源泄漏,編寫異常安全代碼提供了一種解決方案,但它是萬能良藥嗎?使用智能指針,就能保證不會再有資源泄漏嗎?來看下面的代碼:

//header file
void func( shared_ptr<T1> ptr1, shared ptr<T2> ptr2 );

//call func like this
func( shared_ptr<T1>( new T1() ), shared_ptr<T2>( new T2() ) );

上面的函數調用,看起來是安全的,但在現實世界中,其實不然:由於C++並未定義一個表達式的求值順序,因此上述函數調用除了func在最後得到調用之外可以確定,其他的執行序列則很可能被拆分成如下步驟

a.分配內存給T1
b.構造T1對象
c.分配內存給T2   
d.構造T2對象    
e.構造T1的智能指針對象   
f.構造T2的智能指針對象   
g.調用func

或者:

a1.分配內存給T1
b1.分配內存給T2
c1.構造T1對象       
d1.構造T2對象   
e1.構造T1的智能指針對象  
f1.構造T2的智能指針對象  
g1.調用func

上述無論哪種形式的構造序列,如果在 c 或者 d / c1 或者d1失敗,則T1對象分配內存必然泄漏。

爲了解決這個問題,有一個依然使用智能指針的笨辦法:

template<class T>
shared_ptr<T> shared_ptr_new()
{
    return shared_ptr<T>( new T );
}

//call like this
func( shared_ptr_new<T1>(), shared_ptr_new<T2>() );

使用這種辦法,可以解決因爲產生異常導致資源泄漏的問題,然而另一個問題又出現了,如果T1或者T2的構造函數需要提供參數該怎麼辦呢?難道提供很多個重載的版本?——>可以倒是可以,只要你不嫌累,而且有足夠的先見性。

其實,最完美的方案,其實也是最簡單的,就是儘量簡單的書寫代碼,像這樣:

//header file
void func( shared_ptr<T1> ptr1, shared_ptr<T2> ptr2 );

//call func like this
shared_ptr<T1> ptr1( new T1() );
shared_ptr<T2> ptr2( new T2() );
func(ptr1, ptr2  );

這樣簡簡單單的代碼,避免了異常導致的泄漏。又應了那句話:簡單就是美。其實,在一個表達式中,分配多個資源或者需要求多個值等操作都是不安全的。

歸總爲一句話:拋棄臨時對象,讓所有的智能指針都有名字,就可以避免此類問題的發生。

shared_ptr交叉引用導致的泄漏

是否讓每個智能指針都有了名字,就不會再有內存泄漏?不一定,看下面的代碼的輸出,是否會感到驚訝。。。

class CLeader;
class CMember;

class CLeader
{
public:
      CLeader() { cout << "CLeader::CLeader()" << endl; }
      ~CLeader() { cout << "CLeader:;~CLeader() " << endl; }

      std::shared_ptr<CMember> member;
};

class CMember
{
public:
      CMember()  { cout << "CMember::CMember()" << endl; }
      ~CMember() { cout << "CMember::~CMember() " << endl; }

      std::shared_ptr<CLeader> leader;   
};

void TestSharedPtrCrossReference()
{
      cout << "TestCrossReference<<<" << endl;
      boost::shared_ptr<CLeader> ptrleader( new CLeader );
      boost::shared_ptr<CMember> ptrmember( new CMember );

      ptrleader->member = ptrmember;
      ptrmember->leader = ptrleader;

      cout <<"  ptrleader.use_count: " << ptrleader.use_count() << endl;
      cout <<"  ptrmember.use_count: " << ptrmember.use_count() << endl;
}
//output:
CLeader::CLeader()
CMember::CMember()
  ptrleader.use_count: 2
  ptrmember.use_count: 2

從運行結果來看,兩個對象的析構函數都沒有調用,也就是出現了內存泄漏—–>原因在於:TestSharedPtrCrossReference() 函數退出時,兩個shared_ptr對象的引用計數都是2,所以不會釋放對象;

這裏寫圖片描述

這裏出現了常見的交叉引用的問題,這個問題,即使用原生指針互相記錄時也需要格外小心。shared_ptr在這裏栽了跟頭,ptrleader和ptrmember在離開作用域的時候,由於引用計數不爲1,所以最後一次的release操作(shared_ptr析構函數裏面調用)也無法destroy掉所託管的資源。

爲了解決這個問題,可以採用weak_ptr來隔斷交叉引用的迴路,所謂的weak_ptr,是一種弱引用,表示只是對某個對象的一個引用和使用,而不做管理工作。weak_ptr 和 shared_ptr對比:

shared_ptr weak_ptr
強引用 弱引用
強引用存在,則引用的對象必定存在;
只要有一個強引用存在,強引用對象就不能釋放;
是對象存在的一個引用;
即使有弱引用存在,對象仍然可以釋放;
增加對象的引用計數 不增加對象的引用計數
負責資源管理,在引用計數爲0時釋放資源 不負責資源管理
有多個構造函數,可以從任意類型初始化 只能從一個shared_ptr或者weak_ptr對象上進行初始化
zebra stripes 行爲類似原生指針,不過可以用expired()判斷對象是否已經釋放

由於weak_ptr具有上述的一些性質,所以如果把CMember的聲明改成如下形式,就可以解除這種循環,從而每個資源都可以順利釋放。

class CMember
{
public:
      CMember()  { cout << "CMember::CMember()" << endl; }
      ~CMember() { cout << "CMember::~CMember() " << endl; }

      boost::weak_ptr<CLeader> leader;   
};

這種使用weak_ptr的方式,是基於已暴露問題的修正方案,在做設計的時候,一般很難注意到這一點,總之,C++缺少垃圾收集機制,雖然智能指針提供了一個解決方案,但它也難以到達完美,因此,C++的資源管理必須慎之又慎。

類向外傳遞this 與 shared_ptr

可以說,shared_ptr着力解決類對象一級的資源管理,至於類對象內部,shared_ptr暫時還無法管理,那麼這是否會出現問題呢?看如下代碼:

class Point1
{
public:
    Point1() :  X(0), Y(0) { cout << "Point1::Point1(), (" << X << "," << Y << ")" << endl; }
    Point1(int x, int y) :  X(x), Y(y) { cout << "Point1::Point1(int x, int y), (" << X << "," << Y << ")" << endl; }
    ~Point1() { cout << "Point1::~Point1(), (" << X << "," << Y << ")" << endl; }

public:
    Point1* Add(const Point1* rhs) { X += rhs->X; Y += rhs->Y; return this;}

private:
    int X;
    int Y;
};

void TestPoint1Add()
{
    cout << "TestPoint1Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << endl;
    shared_ptr<Point1> p1( new Point1(2,2) );
    shared_ptr<Point1> p2( new Point1(3,3) );

    p2.reset( p1->Add(p2.get()) );
}

輸出爲:
TestPoint1Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Point1::Point1(int x, int y), (2,2)
Point1::Point1(int x, int y), (3,3)
Point1::~Point1(), (3,3)
Point1::~Point1(), (5,5)
Point1::~Point1(), (5411568,5243076)

爲了使類似Point::Add()可以連續進行Add操作成爲可能,Point1定義了Add方法,並返回了this指針(從Effective C++的條款看,這裏最好該以傳值形式返回臨時變量,在此爲了說明問題,暫且不考慮這種設計是否合理,但他就這樣存在了)。在TestPoint1Add()函數中,使用此返回的指針重置了p2,這樣p2和p1就同時管理了同一個對象,但是他們卻互相不知道這事兒,於是悲劇發生了。在作用域結束的時候,他們兩個都去對所管理的資源進行析構,從而出現了上述的輸出。從最後一行輸出也可以看出,所管理的資源,已經處於“無效”的狀態了。

那麼,我們是否可以改變一下呢,讓Add返回一個shared_ptr了呢。我們來看看Point2:

class Point2
{
public:
    Point2() :  X(0), Y(0) { cout << "Point2::Point2(), (" << X << "," << Y << ")" << endl; }
    Point2(int x, int y) :  X(x), Y(y) { cout << "Point2::Point2(int x, int y), (" << X << "," << Y << ")" << endl; }
    ~Point2() { cout << "Point2::~Point2(), (" << X << "," << Y << ")" << endl; }

public:
    shared_ptr<Point2> Add(const Point2* rhs) { X += rhs->X; Y += rhs->Y; return shared_ptr<Point2>(this);}

private:
    int X;
    int Y;
};

void TestPoint2Add()
{
    cout << endl << "TestPoint2Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << endl;
    shared_ptr<Point2> p1( new Point2(2,2) );
    shared_ptr<Point2> p2( new Point2(3,3) );

    p2.swap( p1->Add(p2.get()) );
}

輸出爲:
TestPoint2Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Point2::Point2(int x, int y), (2,2)
Point2::Point2(int x, int y), (3,3)
Point2::~Point2(), (3,3)
Point2::~Point2(), (5,5)
Point2::~Point2(), (3379952,3211460)

從輸出來看,哪怕使用shared_ptr來作爲Add函數的返回值,仍然無濟於事;對象仍然被刪除了兩次;

針對這種情況,shared_ptr的解決方案是:enable_shared_from_this這個模板類,所有需要在內部傳遞this指針的類,都從enable_shared_from_this繼承,在需要傳遞this的時候,使用其成員函數shared_from_this() 來返回一個shared_ptr,運用這種方案,我們改良我們的Point類,得到如下的Point3:

class Point3 : public enable_shared_from_this<Point3>
{
public:
    Point3() :  X(0), Y(0) { cout << "Point3::Point3(), (" << X << "," << Y << ")" << endl; }
    Point3(int x, int y) :  X(x), Y(y) { cout << "Point3::Point3(int x, int y), (" << X << "," << Y << ")" << endl; }
    ~Point3() { cout << "Point3::~Point3(), (" << X << "," << Y << ")" << endl; }

public:
    shared_ptr<Point3> Add(const Point3* rhs) { X += rhs->X; Y += rhs->Y; return shared_from_this();}

private:
    int X;
    int Y;
};

void TestPoint3Add()
{
    cout << endl << "TestPoint3Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" << endl;
    shared_ptr<Point3> p1( new Point3(2,2) );
    shared_ptr<Point3> p2( new Point3(3,3) );

    p2.swap( p1->Add(p2.get()) );
}
輸出爲:
TestPoint3Add() >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Point3::Point3(int x, int y), (2,2)
Point3::Point3(int x, int y), (3,3)
Point3::~Point3(), (3,3)
Point3::~Point3(), (5,5)

從這個輸出可以看出,在這裏的對象析構已經變得正常。因此,在類內部需要傳遞this的場景下,enable_shared_from_this是一個比較靠譜的方案,只不過,要謹慎的脊柱,使用該方案的一個前提,就是類的對象已經被shared_ptr管理,否則就等着拋異常吧。例如:

Point3 p1(10, 10);
Point3 p2(20, 20);

p1.Add( &p2 ); //此處拋異常

上面的代碼會導致crash。那是因爲p1沒有被shared_ptr管理。之所以這樣,是由於shared_ptr的構造函數纔會去初始化enable_shared_from_this相關的引用計數(具體可以參考代碼),所以如果對象沒有被shared_ptr管理,shared_from_this()函數就會出錯。

於是,shared_ptr又引入了注意事項:

  • 若要在內部傳遞this,請考慮從enable_shared_from_this繼承;
  • 若從enable_shared_from_this繼承,則類對象必須讓shared_ptr接管;
  • 如果要使用智能指針,那麼就要保持一致,統統使用智能智能,儘量減少raw pointer裸指針的使用;

總結

  • C++沒有垃圾收集,資源管理需要自己來做;
  • 智能指針可以部分解決資源管理的工作,但是不是萬能的;
  • 使用智能指針的時候,每個shared_ptr 對象都應該有一個名字,也就是避免在一個表達式內做多個資源的初始化;
  • 避免shared_ptr的交叉引用,使用weak_ptr打破交叉引用;
  • 使用enable_shared_from_this 機制來把this 從類內部傳遞出來;
  • 資源管理保持統一風格,要麼使用智能指針,要麼就全部自己管理裸指針;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章