shared_ptr的理解和注意事項

FreeeLinux發表於2017-01-22

昨天把shared_ptr的原始碼分析了一遍,也看了一些有關shared_ptr分析的文章,今天自己來總結一下。

以獨立語句將newed物件置入智慧指標

對於下面這樣一個使用智慧指標的函式呼叫,可能會造成記憶體洩漏:

process(std::shared_ptr<int>(new int(10)), priority());

編譯器沒有規定傳入引數的呼叫順序,可能先執行new表示式,然後執行priority()函式,最後構造shared_ptr物件。如果priority()函式中丟擲異常,那麼new出來的記憶體將得不到釋放。這會造成記憶體洩漏。

所以要使用下面兩種方式:
方式一:

shared_ptr<int> p(new int(10));
process(p, priority());

方式二:

process(std::make_shared<int>(10), priority);

前者即是以獨立語句將new分配過的物件置入只能指標,後者是利用工廠函式,直接生成只能指標。

小知識:make_shared()和普通shared_ptr(new xx)構造方法的區別

make_shared的效率更高。直接使用shared_ptr(new xx)底層實際上涉及兩次記憶體分配,一次是為被管理的物件分配記憶體,一次是為ref_count控制塊分配記憶體。而make_shared則是一次性分配好一大塊記憶體來同時持有被管理的物件和ref_count控制塊。增加了程式碼執行速度,且能避免一些控制塊的薄記資訊(記憶體cookie),潛在減少了程式佔用的空間。不過make_shared有個缺點,使用make_shared會導致物件銷燬和記憶體被回收之間可能會有延遲。因為使用make_shared相當於把被管理物件記憶體和ref_count控制塊記憶體繫結在一起了,我們知道,ref_count控制塊記憶體本來是由use_count和weak_count共同維護的,假設被管理物件引用計數為0了,但是仍然有weak_ptr存在,那麼這一大塊記憶體就不能銷燬;而傳統的shared_ptr(new xx)方式,當物件引用計數為0時,物件記憶體銷燬。當weak_ptr引用計數為0時,ref_count控制塊記憶體銷燬,所有的記憶體都能夠及時清理。

  • 相比於使用new,make函式可以消除程式碼重複,提高異常安全。而且std::make_shared生成的程式碼更小更快。不適用make函式的場合包括自定義刪除器和想要傳遞大括號初始值(這個暫時不太懂)。使用make函式不明智的場合包括:(1)自定義記憶體管理函式的類(2)記憶體緊張的系統,有非常大的物件,然後std::weak_ptr比std::shared_ptr長壽。

注意迴圈引用帶來的記憶體洩漏

迴圈引用就是指,兩個shared_ptr,你持有我,我持有你,雙方都認為對方死了自己才能銷燬,結果兩方都無法銷燬,造成記憶體洩漏。

示例程式碼:

class B;

class A { 
public:
    A(){
        std::cout<<"A ctor"<<std::endl;
    }   
    ~A(){
        std::cout<<"A dtor"<<std::endl;
    }   
    void do_something() {
        if(b_.lock()){
            std::cout<<"lock success, still alive"<<std::endl; 
            std::cout<<"use_count="<<b_.use_count()<<std::endl;  //輸出1
        }   
    }   
public:
    //std::shared_ptr<B> b_;
    std::weak_ptr<B> b_; 
};

class B { 
public:
    B(){
        std::cout<<"B ctor"<<std::endl;
    }   
    ~B(){
        std::cout<<"B dtor"<<std::endl;
    }
public:
    std::shared_ptr<A> a_;
};

int main()
{
    std::shared_ptr<A> pa(new A);
    std::shared_ptr<B> pb(new B);
    std::cout<<"pb->use_count"<<pb.use_count()<<std::endl;
    pa->b_ = pb;
    pb->a_ = pa;
    pa->do_something();

    std::weak_ptr<B> pbb(pb);
    std::cout<<(pbb.lock()).use_count()<<std::endl; //輸出2

    return 0;

我們可以使用weak_ptr來打破這個局面,而不是手動打破迴圈。一方持有另一方的shared_ptr(強引用),而另外一方則持有對方的weak_ptr(弱引用)。與之前的區別是,持有弱引用的一方在使用weak_ptr時,需要使用lock()方法,提升成為shared_ptr,提升失敗說明對方已死,提升成功便可正常使用。這樣就可以打破迴圈引用,避免記憶體洩漏。

程式碼中要注意的一點是,weak_ptr.lock()方法雖然會提升引用計數,但是如果我們僅僅把.lock()用作表示式,那麼實際上是臨時物件temp存在的時候引用計數為2,臨時物件在表示式結束後立即死亡,引用計數又回到1。所以,我們如果要使用weak_ptr提升成為shared_ptr,必須用一個shared_ptr來接收,否則weak_ptr不會提升成為shared_ptr。

類向外傳遞this與shared_ptr

當我們在一個類的成員函式中,想知道自己this所在的shared_ptr時,不可以用shared_ptr(this)直接構造,因為這樣會構造一個獨立的區域性shared_ptr,該shared_ptr銷燬時會釋放掉this。如果外部不知道該物件已經銷燬,再呼叫就會爆炸。所以應該用shared_from_this,所在類需要繼承std::enable_shared_from_this。

示例程式碼:

class A : public std::enable_shared_from_this<A> {  //注意繼承要加模板引數
public:
    A() { std::cout<<"ctor"<<std::endl; }
    ~A() { std::cout<<"dtor"<<std::endl; }
public:
    void func() {
        //std::shared_ptr<A> local_sp(this);   //錯誤用法
        //std::cout<<local_sp.use_count()<<std::endl;  //引用計數為1,因為這個區域性的shared_ptr是用this單獨生成的,和外部沒有關係,所以會造成兩次銷燬doble delete
        std::shared_ptr<A> local_sp = shared_from_this();   //正確用法
        std::cout<<local_sp.use_count()<<std::endl;  //引用計數為2
    }   
};

int main()
{
    std::shared_ptr<A> sp(new A); 
    {   
        sp->func();
    }   

    return 0;
}

上述錯誤情況輸出是:

這裡寫圖片描述

正確情況輸出是:
這裡寫圖片描述

可見,使用this在類內部再造一個shared_ptr是多麼可怕的事情,會造成多次釋放。

在多執行緒環境環境中,我們可能在成員函式中需要將this的智慧指標回撥給別的類物件,就可使用shared_from_this()。這樣,別的物件就能夠知道本物件是否還活著,避免在死亡之後的時候呼叫,產生可怕的後果。

shared_ptr的技術與陷阱

  1. 意外延長物件生命週期。可能容器持有shared_ptr,或者boost::function持有,這都會不經意間延長物件的生命期。我們可能會通過讓他們持有weak_ptr來避免生命期被延長。
  2. 函式引數,一個執行緒只要在最外層持有一個實體,那麼安全不成問題。比如:

    void on_message(const string& msg){
        shared_ptr<foo> f(new foo(msg));  //在最外層有一個實體,然後將shared_ptr傳給誰都可以,因為始終有一個引用計數為1存在。常引用方式傳遞效率更高。
        if(validate(f)){   //const reference
            save(f);  //const reference
        }
    }
    
  3. 虛析構不再是必須。因為shared_ptr有T和Y兩個引數。使用base類的shared_ptr接收derived的shared_ptr,ref_count中維護的指標型別仍是T,即base*型別。而shared_ptr類中維護的才是Y。刪除器還是按老樣子刪除。所以不要虛析構也成。

有一張圖可以看一下:

這裡寫圖片描述

暫時完畢。

相關文章