智能指針的出現解決了對內存的釋放問題,我們瞭解它的內存管理機制有利於我們理解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的繼承關係
不難發現,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的私有化,不得不佩服還有這種寫法,恕我愚鈍了,微軟的寫法值得我們學習。
希望大家有所收穫。
本文難免有所錯誤,如有問題歡迎留言