關於std::shared_ptr與std::enable_shared_from_this循環引用導致的問題

自從C++11有了std::shared_ptr這樣的智能指針,作爲C++程序只要將一個堆上的類對象用std::shared_ptr包裹一下就可以做到內存自動釋放了。看一個例子:

#include "stdafx.h"
#include <memory>

class A
{
public:
    A()
    {
        m_i = 9;
    }

    ~A()
    {
        m_i = 0;
    }

public:
    int     m_i;

};

int _tmain(int argc, _TCHAR* argv[])
{
    {
        std::shared_ptr<A> spa(new A());
    }
    

    return 0;
}


如下圖,上面的代碼new出來一個堆對象A,但是出了作用域後,由於std::shared_ptr對象spa的引用計數減爲0,會自動調用A的析構函數來釋放這塊堆內存:




但是假如,我們有一些開發需求中(也可能是前同事遺留下的代碼),我們需要在一個類中引用自身,即一個類的一個成員變量是一個std::shared_ptr對象,它引用了類對象自身,這裏分爲兩種情況,第一種情況是類對象是棧對象,第二種情況是類對象是堆對象。

我們先看類對象是棧對象的情形,示例代碼如下:

#include "stdafx.h"
#include <memory>

class A : public std::enable_shared_from_this<A>
{
public:
    A()
    {
        m_i = 9;
        //注意:
        //比較好的做法是在構造函數裏面調用shared_from_this()給m_SelfPtr賦值
        //但是很遺憾不能這麼做,如果寫在構造函數裏面程序會直接崩潰
    }
    
    ~A()
    {
        m_i = 0;
    }

    void func()
    {
        m_SelfPtr = shared_from_this();
    }

public:
    int                 m_i;
    std::shared_ptr<A>  m_SelfPtr;

};

int _tmain(int argc, _TCHAR* argv[])
{
    {
        A a;
        a.func();
    }
    

    return 0;
}

上面的代碼,在調用a.func()時程序會直接崩潰,崩潰的原因是調用shared_from_this()函數裏面,看一下崩潰的調用堆棧:


我們來看下崩潰的原因,看下shared_from_this()函數的調用細節:







也就是說shared_from_this()函數內部會先調用shared_ptr的構造函數去構造一個shared_ptr對象,參數是自己的成員變量_Wptr,這是一個std::weak_ptr:

private:
	template<class _Ty1,
		class _Ty2>
		friend void _Do_enable(
			_Ty1 *,
			enable_shared_from_this<_Ty2>*,
			_Ref_count_base *);

	mutable weak_ptr<_Ty> _Wptr;


而shared_ptr的構造函數裏面又會調用reset()先釋放之前的對象引用,如果這個之前的對象就是_Wptr這個指針去引用,但是現在_Wptr是空的,就拋出一個異常。_Wptr之所以爲空,是這個指針引用的對象並沒有被任何智能指針所包裹(A的對象a是棧變量)。這就是崩潰的原因。

我們來接着看下A對象是堆對象的情形:

class A : public std::enable_shared_from_this<A>
{
public:
    A()
    {
        m_i = 9;
        //注意:
        //比較好的做法是在構造函數裏面調用shared_from_this()給m_SelfPtr賦值
        //但是很遺憾不能這麼做,如果寫在構造函數裏面程序會直接崩潰
    }
    
    ~A()
    {
        m_i = 0;
    }

    void func()
    {
        m_SelfPtr = shared_from_this();
    }

public:
    int                 m_i;
    std::shared_ptr<A>  m_SelfPtr;

};

int _tmain(int argc, _TCHAR* argv[])
{
    {
        std::shared_ptr<A> spa(new A());
        spa->func();
    }
    

    return 0;
}

這次不會崩潰了,但是遺憾的是,這個new出來的A對象的堆內存再也不會釋放了(當然程序退出靠操作系統回收不算)。爲啥不會釋放呢?我們來分析下原因:

要想堆上的A被釋放,那麼至少需要所有指向A的std::shared_ptr對象都不再引用A,但是A的成員變量只有在A自己被釋放的時候纔會不再引用A。反過來說,A的成員變量m_SelfPtr等着A對象本身釋放,而A作爲堆對象釋放的條件是所有引用它的的std::shared_ptr釋放。這就相互矛盾了。這種情形導致,這樣的A對象永遠不會被自動釋放。我們使用std::weak_ptr來看看最終這個A的引用計數是多少:

#include "stdafx.h"
#include <memory>

class A : public std::enable_shared_from_this<A>
{
public:
    A()
    {
        m_i = 9;
        //注意:
        //比較好的做法是在構造函數裏面調用shared_from_this()給m_SelfPtr賦值
        //但是很遺憾不能這麼做,如果寫在構造函數裏面程序會直接崩潰
    }
    
    ~A()
    {
        m_i = 0;
    }

    void func()
    {
        m_SelfPtr = shared_from_this();
    }

public:
    int                 m_i;
    std::shared_ptr<A>  m_SelfPtr;

};

int _tmain(int argc, _TCHAR* argv[])
{
    std::weak_ptr<A> spwa;
    {
        std::shared_ptr<A> spa(new A());
        spa->func();
        spwa = spa;
    }

    printf("spwa usecount: %d\n", spwa.use_count());
    

    return 0;
}




確實和我們分析的一樣,這個堆上的A引用計數永遠是A了,所以不會被釋放了。


那有什麼解決方案呢?

我們可以在增加一個成員函數,在不需要A時,主動釋放這個智能指針的成員變量引用的對象:

#include "stdafx.h"
#include <memory>

class A : public std::enable_shared_from_this<A>
{
public:
    A()
    {
        m_i = 9;
        //注意:
        //比較好的做法是在構造函數裏面調用shared_from_this()給m_SelfPtr賦值
        //但是很遺憾不能這麼做,如果寫在構造函數裏面程序會直接崩潰
    }
    
    ~A()
    {
        m_i = 0;
    }

    void func()
    {
        m_SelfPtr = shared_from_this();
    }

    void release()
    {
        m_SelfPtr.reset();
    }

public:
    int                 m_i;
    std::shared_ptr<A>  m_SelfPtr;

};

int _tmain(int argc, _TCHAR* argv[])
{
    std::weak_ptr<A> spwa;
    {
        std::shared_ptr<A> spa(new A());
        spa->func();
        spa->release();
        spwa = spa;
    }

    printf("spwa usecount: %d\n", spwa.use_count());
    

    return 0;
}


這樣,程序就會自動調用A的析構函數來釋放自己呢。但是!!!這樣人爲地增加一個release()函數相當於手工調用了delete,使用智能指針還有什麼意義,我們還得人工管理內存釋放。


綜合下來,這種在對象內部引用自己的智能指針是一種非常不好的設計,個人覺得還是要杜絕這種錯誤的用法。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章