com智能指針實現原理

智能指針的出現解決了對內存的釋放問題,我們瞭解它的內存管理機制有利於我們理解C++、boost中的智能指針以及其他例如cocos中的內存管理。

目錄
一、引用計數的原理
二、引用計數的實現與使用
三、引用計數的優化
四、智能指針的實現原理與使用

一、引用計數的原理

困擾C++或者C語言的就是資源釋放的問題,編程中往往會一不小心就導致內存泄漏,所以我們必須解決內存資源何時釋放的問題,通過引用計數進行內存管理。

首先我們需要了解內存資源何時釋放?
我們通常的做法是在使用完一個對象後,在函數的最後執行delete,這樣確實能釋放資源。但是這裏會有一個是否“及時”釋放的問題,如果一個程序,例如在一個遊戲中所有的資源在不用時都沒有及時釋放,這個程序在運行中所佔用的內存將是巨大的,玩家的感受就是很卡。自然,就需要我們的引用計數技術,引用計數技術就是用來管理對象生命期的一種技術。

引用計數的原理
- 對象O可能同時被外界A,外界B,外界C引用。也就是說外界A,外界B,外界C可能都在使用對象O。
- 每次當對象被外界引用時,計數器就自增1。
- 每次當外界不用對象時,計數器就自減1。
- 在計數值爲零時,對象本身執行delete this,銷燬自己的資源
- 引用計數使得對象通過計數能夠知道何時對象不再被使用,然後及時地刪除自身所佔的內存資源。
- IUnknown接口的AddRef與Release就是引用計數的實現方法。

二、引用計數的實現與使用

#include <iostream>
#include <Unknwn.h>
#include <atlcomcli.h>
using namespace std;

// {D16ACAA2-0C01-4673-97CB-FB941C1D48A1}
static const IID IID_IX =
{ 0xd16acaa2, 0xc01, 0x4673,{ 0x97, 0xcb, 0xfb, 0x94, 0x1c, 0x1d, 0x48, 0xa1 } };

// {A5802CE5-C507-44C5-8CB6-5F5D8BC78CFC}
static const IID IID_IY =
{ 0xa5802ce5, 0xc507, 0x44c5,{ 0x8c, 0xb6, 0x5f, 0x5d, 0x8b, 0xc7, 0x8c, 0xfc } };

// 接口 IX
interface IX : public IUnknown
{
    virtual void Fx() = 0;
};

// 接口 IY
interface IY : public IUnknown
{
    virtual void Fy() = 0;
};

// 組件 CCom
class CCom : public IX, public IY
{
public:
    CCom()
    {
        m_ulCount = 0;
        AddRef();
    }
    ~CCom()
    {
        cout << "CCom 析構函數被調用" << endl;
    }

    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject)
    {
        if (riid == IID_IUnknown)
        {
            //*ppvObject = static_cast<IUnknown*>(static_cast<IX*>(this));
            *ppvObject = static_cast<IX*>(this);
        }
        else if (riid == IID_IX)
        {
            *ppvObject = static_cast<IX*>(this);
        }
        else if (riid == IID_IY)
        {
            *ppvObject = static_cast<IY*>(this);
        }
        else
        {
            *ppvObject = NULL;
            return E_NOINTERFACE;
        }

        AddRef();// 進行引用計數加一

        return S_OK;
    }

    virtual ULONG STDMETHODCALLTYPE AddRef(void)
    {
        return InterlockedIncrement(&m_ulCount);
    }

    virtual ULONG STDMETHODCALLTYPE Release(void)
    {
        if (InterlockedDecrement(&m_ulCount) == 0)
        {
            delete this;
            return 0;
        }
        return m_ulCount;
    }

    virtual void Fx() override
    {
        cout << "This is Fx()" << endl;
    }


    virtual void Fy() override
    {
        cout << "This is Fy()" << endl;
    }

private:
    ULONG m_ulCount;// 記錄引用的次數
};

int main()
{
    HRESULT hr = 0;

    // 創建組件
    CCom* pCom = new CCom;

    // 從組件查找接口,然後再調用接口實現的功能
    IUnknown* pIUnknown = NULL;
    hr = pCom->QueryInterface(IID_IUnknown, (void**)&pIUnknown);// 查詢 IUnknown 接口
    if (SUCCEEDED(hr))
    {
        pIUnknown->Release();

        IX* pIX = NULL;
        hr = pCom->QueryInterface(IID_IX, (void**)&pIX);// 查詢 IX 接口
        if (SUCCEEDED(hr))
        {
            pIX->Fx();
            pIX->Release();// 調用Release進行引用計數減一
        }

        IY* pIY = NULL;
        hr = pCom->QueryInterface(IID_IY, (void**)&pIY);// 查詢 IY 接口
        if (SUCCEEDED(hr))
        {
            pIY->Fy();
            pIY->Release();// 調用Release進行引用計數減一
        }
    }

    pCom->Release();
//  delete pCom;
//  pCom = NULL;

    return 0;
}

主要的實現是實現繼承於IUnknown的AddRef和Release接口進行引用計數的管理。
AddRef:對用於引用計數的變量m_ulCount進行原子操作的加一(原子操作防止線程同步時出現錯誤)。
Release:對用於引用計數的變量m_ulCount進行原子操作的減一,當引用計數爲0時,就調用delete this;摧毀自己,實現對資源的釋放。

對於引用計數砈使用:在聲明組件對象的時候,調用構造函數進行引用計數加一。調用QueryInterface來查詢接口時,如果查詢成功進行引用計數加一,如果不使用是調用Release進行引用計數減一,在最後調用Release時,引用計數爲0釋放資源。

三、引用計數的優化

引用計數的優化原則
1、輸入參數原則
- 輸入參數指的是給函數傳遞某個值的參數。在函數體中將會使用這個值但卻不會修改它或將其返回給調用者。在C++中,輸入參數實際上就是那些按值傳遞的參數。
- 對傳入函數的接口指針,無需調用AddRef與Release
2、局部變量原則
- 對於局部複製的接口指針,由於它們只是在函數的生命期內才存在,因此無需調用AddRef與Release

輸入參數原則

void Fun(IX *pIXParam)   //參數傳遞存在賦值過程
{
        //pIXParam->AddRef();   //可優化,註釋掉
        pIXParam->Fx1();
        pIXParam->Fx2();
        //pIXParam->Release();  //可優化,註釋掉
}

void main()
{
    ...
    Fun(pIX);
    ...
}

在main中調用Fun時,傳遞的pIX只是進行了按值傳遞,即函數的參數只是實際參數(注意這裏的參數只是一個四字節的地址)的拷貝,pIXParam值與入參pIX址完全相同,而並沒有進行對象的引用,所以並不需要AddRef與Release。

局部變量原則

void Fun(IX *pIX)
{
        IX *pIX2 = pIX;
        //pIX2->AddRef();   //可優化,註釋掉
        pIX2->Fx1();
        pIX2->Fx2();
        //pIX2->Release();  //可優化,註釋掉
}

函數運行在堆棧上,在我們調用函數的時候開始壓棧,同時對局部變量申請棧空間,在函數執行完畢的時候,進行出棧操作,實現對局部資源的清理(在一定時間裏,其實在棧上的某些值是還能存在的,因爲出棧後棧空間上的值並沒有清零,但是我們不能抱有調用棧上資源的心態)
從上不能理解,我們局部變量的生存週期只是在函數體內,而函數體結束時會自動清理資源,於是我們並不用進行AddRef與Release操作。

四、智能指針的實現原理與使用

此時你可能很疑惑,雖然引用計數帶來了高效的內存資源管理方法,能及時地釋放不再使用的資源。但卻帶來了編碼的麻煩,如果我哪一次忘記了調用Release,豈不是同樣會造成內存泄漏,而且每次都還要調用Release,真是麻煩。不用着急,前輩們幫我們解決了這個問題,那就是智能指針。

我們先來看看CComPtr的實現

template <class T>
class CComPtrBase
{
protected:
    CComPtrBase() throw()
    {
        p = NULL;
    }
    CComPtrBase(_Inout_opt_ T* lp) throw()
    {
        p = lp;
        if (p != NULL)
            p->AddRef();
    }
    void Swap(CComPtrBase& other)
    {
        T* pTemp = p;
        p = other.p;
        other.p = pTemp;
    }
public:
    typedef T _PtrClass;
    ~CComPtrBase() throw()
    {
        if (p)
            p->Release();
    }
    operator T*() const throw()
    {
        return p;
    }
    T& operator*() const
    {
        ATLENSURE(p!=NULL);
        return *p;
    }
    T** operator&() throw()
    {
        ATLASSERT(p==NULL);
        return &p;
    }
    _NoAddRefReleaseOnCComPtr<T>* operator->() const throw()
    {
        ATLASSERT(p!=NULL);
        return (_NoAddRefReleaseOnCComPtr<T>*)p;
    }
    bool operator!() const throw()
    {   
        return (p == NULL);
    }
    bool operator<(_In_opt_ T* pT) const throw()
    {
        return p < pT;
    }
    bool operator!=(_In_opt_ T* pT) const
    {
        return !operator==(pT);
    }
    bool operator==(_In_opt_ T* pT) const throw()
    {
        return p == pT;
    }

    // Release the interface and set to NULL
    void Release() throw()
    {
        T* pTemp = p;
        if (pTemp)
        {
            p = NULL;
            pTemp->Release();
        }
    }
    // Compare two objects for equivalence
    bool IsEqualObject(_Inout_opt_ IUnknown* pOther) throw()
    {
        if (p == NULL && pOther == NULL)
            return true;    // They are both NULL objects

        if (p == NULL || pOther == NULL)
            return false;   // One is NULL the other is not

        CComPtr<IUnknown> punk1;
        CComPtr<IUnknown> punk2;
        p->QueryInterface(__uuidof(IUnknown), (void**)&punk1);
        pOther->QueryInterface(__uuidof(IUnknown), (void**)&punk2);
        return punk1 == punk2;
    }
    // Attach to an existing interface (does not AddRef)
    void Attach(_In_opt_ T* p2) throw()
    {
        if (p)
        {
            ULONG ref = p->Release();
            (ref);
            ATLASSERT(ref != 0 || p2 != p);
        }
        p = p2;
    }
    // Detach the interface (does not Release)
    T* Detach() throw()
    {
        T* pt = p;
        p = NULL;
        return pt;
    }
    _Check_return_ HRESULT CopyTo(_COM_Outptr_result_maybenull_ T** ppT) throw()
    {
        ATLASSERT(ppT != NULL);
        if (ppT == NULL)
            return E_POINTER;
        *ppT = p;
        if (p)
            p->AddRef();
        return S_OK;
    }
    _Check_return_ HRESULT SetSite(_Inout_opt_ IUnknown* punkParent) throw()
    {
        return AtlSetChildSite(p, punkParent);
    }
    _Check_return_ HRESULT Advise(
        _Inout_ IUnknown* pUnk, 
        _In_ const IID& iid, 
        _Out_ LPDWORD pdw) throw()
    {
        return AtlAdvise(p, pUnk, iid, pdw);
    }
    _Check_return_ HRESULT CoCreateInstance(
        _In_ REFCLSID rclsid, 
        _Inout_opt_ LPUNKNOWN pUnkOuter = NULL, 
        _In_ DWORD dwClsContext = CLSCTX_ALL) throw()
    {
        ATLASSERT(p == NULL);
        return ::CoCreateInstance(rclsid, pUnkOuter, dwClsContext, __uuidof(T), (void**)&p);
    }
#ifdef _ATL_USE_WINAPI_FAMILY_DESKTOP_APP
    _Check_return_ HRESULT CoCreateInstance(
        _In_z_ LPCOLESTR szProgID, 
        _Inout_opt_ LPUNKNOWN pUnkOuter = NULL, 
        _In_ DWORD dwClsContext = CLSCTX_ALL) throw()
    {
        CLSID clsid;
        HRESULT hr = CLSIDFromProgID(szProgID, &clsid);
        ATLASSERT(p == NULL);
        if (SUCCEEDED(hr))
            hr = ::CoCreateInstance(clsid, pUnkOuter, dwClsContext, __uuidof(T), (void**)&p);
        return hr;
    }
#endif // _ATL_USE_WINAPI_FAMILY_DESKTOP_APP
    template <class Q>
    _Check_return_ HRESULT QueryInterface(_Outptr_ Q** pp) const throw()
    {
        ATLASSERT(pp != NULL);
        return p->QueryInterface(__uuidof(Q), (void**)pp);
    }
    T* p;
};

template <class T>
class CComPtr : 
    public CComPtrBase<T>
{
public:
    CComPtr() throw()
    {
    }
    CComPtr(_Inout_opt_ T* lp) throw() :
        CComPtrBase<T>(lp)
    {
    }
    CComPtr(_Inout_ const CComPtr<T>& lp) throw() :
        CComPtrBase<T>(lp.p)
    {   
    }
    T* operator=(_Inout_opt_ T* lp) throw()
    {
        if(*this!=lp)
        {
            CComPtr(lp).Swap(*this);
        }
        return *this;
    }
    template <typename Q>
    T* operator=(_Inout_ const CComPtr<Q>& lp) throw()
    {
        if( !IsEqualObject(lp) )
        {
            return static_cast<T*>(AtlComQIPtrAssign((IUnknown**)&p, lp, __uuidof(T)));
        }
        return *this;
    }
    T* operator=(_Inout_ const CComPtr<T>& lp) throw()
    {
        if(*this!=lp)
        {
            CComPtr(lp).Swap(*this);
        }
        return *this;
    }   
    CComPtr(_Inout_ CComPtr<T>&& lp) throw() :  
        CComPtrBase<T>()
    {   
        lp.Swap(*this);
    }   
    T* operator=(_Inout_ CComPtr<T>&& lp) throw()
    {           
        if (*this != lp)
        {
            CComPtr(static_cast<CComPtr&&>(lp)).Swap(*this);
        }
        return *this;       
    }
};

CComPtr的繼承關係
CCom
不難發現,CComPtr智能指針類中,其實就是維護了我們的對象指針(T* p;),類中對原始指針的->等操作符都進行了相應運算符重載,同時也封裝了一些增強的功能,例如IsEqualObject用於判斷是否爲同一個對象等。如果我們需要原始指針,通過CCom* pCom = pComPtr;直接賦值就能獲得。

當我們使用智能指針時,在構造函數中,父類CComPtrBase進行構造時會自動調用一次AddRef,CComPtrBase析構的時候調用一次Release,引用計數減一。這個過程並不需要我們自己去調用Release來進行引用計數的減一。

我們還是來切身體會下智能指針的使用

#include <iostream>
#include <Unknwn.h>
#include <atlcomcli.h>
using namespace std;

// {D16ACAA2-0C01-4673-97CB-FB941C1D48A1}
static const IID IID_IX =
{ 0xd16acaa2, 0xc01, 0x4673,{ 0x97, 0xcb, 0xfb, 0x94, 0x1c, 0x1d, 0x48, 0xa1 } };

// {A5802CE5-C507-44C5-8CB6-5F5D8BC78CFC}
static const IID IID_IY =
{ 0xa5802ce5, 0xc507, 0x44c5,{ 0x8c, 0xb6, 0x5f, 0x5d, 0x8b, 0xc7, 0x8c, 0xfc } };

// 接口 IX
interface IX : public IUnknown
{
    virtual void Fx1() = 0;
    virtual void Fx2() = 0;
};

// 接口 IY
interface IY : public IUnknown
{
    virtual void Fy1() = 0;
    virtual void Fy2() = 0;
};

// 組件 CCom
class CCom : public IX, public IY
{
public:
    CCom()
    {
        m_ulCount = 0;
        //AddRef();
    }
    ~CCom()
    {
        cout << "CCom 析構函數被調用" << endl;
    }

    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject)
    {
        if (riid == IID_IUnknown)
        {
            *ppvObject = static_cast<IX*>(this);
        }
        else if (riid == IID_IX)
        {
            *ppvObject = static_cast<IX*>(this);
        }
        else if (riid == IID_IY)
        {
            *ppvObject = static_cast<IY*>(this);
        }
        else
        {
            *ppvObject = NULL;
            return E_NOINTERFACE;
        }

        AddRef();// 進行引用計數加一

        return S_OK;
    }

    virtual ULONG STDMETHODCALLTYPE AddRef(void)
    {
        return InterlockedIncrement(&m_ulCount);
    }

    virtual ULONG STDMETHODCALLTYPE Release(void)
    {
        if (InterlockedDecrement(&m_ulCount) == 0)
        {
            delete this;
            return 0;
        }
        return m_ulCount;
    }

    virtual void Fx1() override
    {
        cout << "This is Fx1()" << endl;
    }

    virtual void Fx2() override
    {
        cout << "This is Fx2()" << endl;
    }

    virtual void Fy1() override
    {
        cout << "This is Fy1()" << endl;
    }

    virtual void Fy2() override
    {
        cout << "This is Fy2()" << endl;
    }

private:
    ULONG m_ulCount;
};

template <class T>
class NoAddRelease :public T  
{
    virtual ULONG STDMETHODCALLTYPE AddRef(void) = 0;
    virtual ULONG STDMETHODCALLTYPE Release(void) = 0;
};

void Test_CComPtr()
{
    HRESULT hr = 0;
    //CCom*p = new CCom;
    //((NoAddRelease<CCom>*)p)->Release();

    // 創建組件
    //CComPtr<CCom> pComPtr = CComPtr<CCom>(new CCom);
    //CComPtr<CCom> pComPtr = new CCom;
    CComPtr<CCom> pComPtr(new CCom);

    // 從組件查找接口,然後再調用接口實現的功能
    CComPtr<IUnknown> pIUnknownPtr = NULL;
    hr = pComPtr->QueryInterface(IID_IUnknown, (void**)&pIUnknownPtr);// 查詢 IUnknown 接口
    if (SUCCEEDED(hr))
    {
        CComPtr<IX> pIXPtr = NULL;
        hr = pComPtr->QueryInterface(IID_IX, (void**)&pIXPtr);// 查詢 IX 接口
        if (SUCCEEDED(hr))
        {
            pIXPtr->Fx1();
            pIXPtr->Fx2();
        }

        // 查詢 IY 接口
        CComPtr<IY> pIYPtr = NULL;
        hr = pComPtr->QueryInterface(IID_IY, (void**)&pIYPtr);
        if (SUCCEEDED(hr))
        {
            pIYPtr->Fy1();
            pIYPtr->Fy2();
        }
    }
}

int main()
{
    Test_CComPtr();

    return 0;
}

注意:我們這裏上面程序的不同,在構造函數裏面這裏並沒有實現AddRef,因爲聲明智能指針時它會自動調用AddRef幫我們進行引用計數加一。

pComPtr聲明的時候,引用計數加一,在函數結束的時候智能指針pComPtr進行析構,調用引用計數減一。
使用QueryInterface查詢IUnknown接口的時候,引用計數加一,在函數結束的時候智能指針pIUnknownPtr進行析構,調用引用計數減一。
使用QueryInterface查詢IX接口的時候,引用計數加一,if語句結束的時候智能指針pIXPtr進行析構,調用引用計數減一。
使用QueryInterface查詢IY接口的時候,引用計數加一,if語句結束的時候智能指針pIYPtr進行析構,調用引用計數減一。

這整個過程並不需要我們去釋放資源,在函數結束的時候,引用計數會減爲0,自動幫我們釋放資源。

聰明的你一定發現了一些問題
此時會不會出現用戶突然自己調用了Release的情況呢?當然不會,因爲Release和AddRef都是私有的。初次我是在網上看到了,但是沒有說明原因,隨後懷着對於實現原理的好奇,我跟進去看了下實現,發現智能指針CComPtr類中實現的CCom類中Release和AddRef是屬於public,而且我們實現的CCom類中Release和AddRef是也是屬於public啊,有點蒙圈,那麼這是怎麼回事的呢?

pComPtr->Release();會調用智能指針類中的->運算符,問題必然是在->運算符中

_NoAddRefReleaseOnCComPtr<T>* operator->() const throw()
{
    ATLASSERT(p!=NULL);
    return (_NoAddRefReleaseOnCComPtr<T>*)p;
}

這裏調用了一個模板類_NoAddRefReleaseOnCComPtr進行強制轉換。而Release和AddRef實現私有化就是該類的作用,我們接下來看下_NoAddRefReleaseOnCComPtr高明的實現

template <class T>
class _NoAddRefReleaseOnCComPtr : 
    public T
{
    private:
        STDMETHOD_(ULONG, AddRef)()=0;
        STDMETHOD_(ULONG, Release)()=0;
};

就是在該類中實現了對Release和AddRef的私有化,不得不佩服還有這種寫法,恕我愚鈍了,微軟的寫法值得我們學習。

希望大家有所收穫。
本文難免有所錯誤,如有問題歡迎留言

發佈了54 篇原創文章 · 獲贊 38 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章