您的位置:首页 > 其它

com智能指针实现原理

2017-12-13 22:45 134 查看
智能指针的出现解决了对内存的释放问题,我们了解它的内存管理机制有利于我们理解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()
{

12bd2
...
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的私有化,不得不佩服还有这种写法,恕我愚钝了,微软的写法值得我们学习。

希望大家有所收获。

本文难免有所错误,如有问题欢迎留言
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  智能指针 com