ATL智能指针类剖析

来源:互联网 发布:电力设计院知乎 编辑:程序博客网 时间:2024/05/08 04:07

 

CComPtr

CComPtr派生自模板类CComPtrBase<T>,T代表某个COM接口。CComPtrBase<T>类只有一个成员变量T* p。下面是它们的代码分析:

构造函数

protected:
CComPtrBase() throw()
{
p = NULL;
}
CComPtrBase(_In_ int nNull) throw()
{
ATLASSERT(nNull == 0);
(void)nNull;
p = NULL;
}
CComPtrBase(_In_opt_ T* lp) throw()
{
p = lp;
if (p != NULL)
p->AddRef();
}

构造函数都被限定为protected,这样保证了无法直接创建CComPtrBase<T>对象,必须有子类的构造函数来调用它们。默认构 造函数和整数0作为参数的构造函数将p初始化为NULL。第三种形式会保存外部接口指针给p,并调用AddRef。

CComPtrBase没有提供AddRef成员函数供外部使用,为了生命周期一致,这是必要的。所以也不要通过operator->来调用内部p的AddRef方法。

CComPtr<T>增加了拷贝构造函数:

CComPtr(_In_ const CComPtr<T>& lp) throw() :
CComPtrBase<T>(lp.p)
{
}

本质上还是调用基类的第三种构造函数,因此也会调用p->AddRef方法。

析构函数

public:
~CComPtrBase() throw()
{
if (p)
p->Release();
}

注意析构函数没有virtual关键字,按照我们通常的认识,一个类如果被设计成被别的类继承,它需要一个虚析构函数,但是这里没有。为什么呢?因为它的子类CComPtr<T>没有任何成员变量,因此当我们如下调用的时候,并不会造成内存泄露。

CComPtrBase<IUnknown>* p=new CComPtr<IUnknown>();
delete p;

但是,这并不是一个很好的方法,虽然看似这里省了点虚函数调用的开销,但是谁又能保证不会有哪个人突然自己创造了一个CComPtrBase<T>的子类,然后加了很多自己的成员变量,而如果他没有看过CComPtrBase<T>的代码,结果 会怎么样?我认为这的确是个设计不良!

析构函数保证了退栈的时候,如果p不为NULL,则会被调用接口的Release函数。CComPtr<T>没有提供析构函数,由于没有成员变量拥有析构函数,父类也没有虚析构函数,所以编译器也不会生成默认析构函数。

Release

作为一个原则,要记住的是我们总应该调用CComPtrBase::Release,而不是通过operator->获得的接口指针来调用Release方法。为什么呢?再看看上面的代码,析构函数在p不为NULL不为的时候,会调用Release。如果我们 直接通过->来调用Release,我们又忘记将成员变量p设置为NULL设置为的话,那么析构函数会进行第二次Release调用,结果当然会导 致错误。下面是可靠地CComPtrBase::Release方法的代码:
void Release() throw()
{
T* pTemp = p;
if (pTemp)
{
p = NULL;
pTemp->Release();
}
}

为什么不直接写成p->Release;p=NULL;而要引入一个pTemp变量呢,这是因为如果这样的话,在下面情况下,会出现两次调用接口的Release方法:
1)假设B类对象又拥有一个A类对象的指针作为成员变量pA,而A类对象pA又有一个成员变量CComPtr<T> sp指向B类对象;
2) 当用户调用pA->sp.Release方法,如果Release实现代码是这样:
if(p)
{
p->Release;
p=NULL;
}
当执行到p->Release时,也就是调用B对象的Release方法;
3)假设B::Release方法内部释放了A对象(delete pA),也就是导致A对象的析构函数被调用
4)A对象的析构函数的调用又导致了A对象内部CComPtr<T> sp的析构函数被调用
5)CComPtr<T> sp的Release方法被再次调用,这时候sp.p仍然不为NULL,因此p->Release会被调用第二次,崩溃了。

AddRef和Release保险机制


下面是个小技巧,懒得翻译了,直接拷贝自"ATL Internals"第二版。

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

This is a simple template class whose only purpose is to make the AddRef and Release methods inaccessible:

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

The _NoAddRefReleaseOnCComPtr<T> template class derives from the interface being returned. Therefore, it inherits all the methods of the interface. The class then overrides the AddRef and Release methods, making them private and purely virtual. Now you get the following compiler error when you use the arrow operator to call either of these methods:

error C2248: 'Release' : cannot access private member declared
in class 'ATL::_NoAddRefReleaseOnCComPtr<T>'

operator =

CComPtrBase<T>没有提供赋值操作,CComPtr提供了下面几个:
T* operator=(_In_opt_ T* lp) throw()
{
if(*this!=lp)
{
return static_cast<T*>(AtlComPtrAssign((IUnknown**)&p, lp));
}
return *this;
}
template <typename Q>
T* operator=(_In_ const CComPtr<Q>& lp) throw()
{
if( !IsEqualObject(lp) )
{
return static_cast<T*>(AtlComQIPtrAssign((IUnknown**)&p, lp, __uuidof(T)));
}
return *this;
}
T* operator=(_In_ const CComPtr<T>& lp) throw()
{
if(*this!=lp)
{
return static_cast<T*>(AtlComPtrAssign((IUnknown**)&p, lp));
}
return *this;
}
相关的ATL函数如下:
ATLINLINE ATLAPI_(IUnknown*) AtlComPtrAssign(_Inout_opt_ _Deref_post_opt_valid_ IUnknown** pp, _In_opt_ IUnknown* lp)
{
if (pp == NULL)
return NULL;

if (lp != NULL)
lp->AddRef();
if (*pp)
(*pp)->Release();
*pp = lp;
return lp;
}

TLINLINE ATLAPI_(IUnknown*) AtlComQIPtrAssign(_Inout_opt_ _Deref_post_opt_valid_ IUnknown** pp, _In_opt_ IUnknown* lp, REFIID riid)
{
if (pp == NULL)
return NULL;

IUnknown* pTemp = *pp;
*pp = NULL;
if (lp != NULL)
lp->QueryInterface(riid, (void**)pp);
if (pTemp)
pTemp->Release();
return *pp;
}

bool IsEqualObject(_In_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;
}

主要功能都是:
1)先判断两个接口是否相同,不同则继续
2)如果p不为NULL,调用p->Release
3)将接口直接赋值给p或者通过接口查询方式初始化p
4)保证接口被赋值后AddRef被调用

Attach和Detach方法

void Attach(_In_opt_ T* p2) throw()
{
if (p)
p->Release();
p = p2;
}
// Detach the interface (does not Release)
T* Detach() throw()
{
T* pt = p;
p = NULL;
return pt;
}

Attach方法先释放原先p所指接口,然后接受新接口,注意,不会导致AddRef调用。
Detach方法将p设置为NULLL,然后返回接口。如果外部调用需要保存返回的接口,否则就遗失了。

CopyTo

_Check_return_ HRESULT CopyTo(_Deref_out_opt_ T** ppT) throw()
{
ATLASSERT(ppT != NULL);
if (ppT == NULL)
return E_POINTER;
*ppT = p;
if (p)
p->AddRef();
return S_OK;
}
将自己的接口拷贝个参数指针,并调用AddRef。

operator*

T& operator*() const
{
ATLENSURE(p!=NULL);
return *p;
}

有了这个操作,我们就可以使得CComPtr拥有和普通指针同样的行为,通过*p来获得p所指对象的引用。

operator T*

operator T*() const throw()
{
return p;
}
这是个类型转换操作。调用方法如下:
CComPtr<IUnknown> sp=...
IUnknown* p=(IUnknown*)sp;

不能使用的operator&

//The assert on operator& usually indicates a bug. If this is really
//what is needed, however, take the address of the p member explicitly.
T** operator&() throw()
{
ATLASSERT(p==NULL);
return &p;
}

当你的CComPtr的p成员变量(集成而来的)不为NULL时,debug模式下会弹出错误对话框。看看上面的注释,这个函数被设计用来防止对智能指针直接调用取地址操作符的。实在要用,请用这种方法:&sp.p。

判断是否指向同一个COM对象的接口

bool operator==(_In_opt_ T* pT) const throw()
{
return p == pT;
}

bool operator!=(_In_opt_ T* pT) const
{
return !operator==(pT);
}

判断是否指向NULL

bool operator!() const throw()
{
return (p == NULL);
}
如果成员变量p为空,则返回true


常用方法和常见错误

由于ATL3.0以后版本的改进,常见的->Release方法已经不能通过编译了,我们再也不会犯这个错误。
CComPtr<T>对象可以作为参数进行值传递和引用传递,也可以做为返回值。放心使用吧。
下面方法是错误的:
IA *pA=NULL;
p->QueryInterfaces(...,&pA);//从某个已有接口查询获取IA
CComPtr<IA> spA(pA)

QueryInterface方法会导致pA->AddRef被调用一次,CComPtr<IA>构造函数又会调用一次AddRef,而最后spA析构时只调用了一次Release,因此COM对象没有被销毁,内存泄露了。正确做法如下:

IA *pA=NULL;
p->QueryInterfaces(...,&pA);
CComPtr<IA> spA;
spA.Attach(pA);
或者
CComPtr<IA> spA(pA)
p->QueryInterfaces(...,&pA.p);

总之,引用计数何时增加和减少心里要警惕,看看源代码还是非常必要的。

CComQIPtr类

继承自CComPtr<T>类,增加了一个模板参数IID,并且有默认的参数值。

template <class T, const IID* piid = &__uuidof(T)>
class CComQIPtr : public CComPtr<T>
{
public:
CComQIPtr() throw()
{
}
CComQIPtr(_In_opt_ T* lp) throw() :
CComPtr<T>(lp)
{
}
CComQIPtr(_In_ const CComQIPtr<T,piid>& lp) throw() :
CComPtr<T>(lp.p)
{
}
CComQIPtr(_In_opt_ IUnknown* lp) throw()
{
if (lp != NULL)
lp->QueryInterface(*piid, (void **)&p);
}
T* operator=(_In_opt_ T* lp) throw()
{
if(*this!=lp)
{
return static_cast<T*>(AtlComPtrAssign((IUnknown**)&p, lp));
}
return *this;
}
T* operator=(_In_ const CComQIPtr<T,piid>& lp) throw()
{
if(*this!=lp)
{
return static_cast<T*>(AtlComPtrAssign((IUnknown**)&p, lp.p));
}
return *this;
}
T* operator=(_In_opt_ IUnknown* lp) throw()
{
if(*this!=lp)
{
return static_cast<T*>(AtlComQIPtrAssign((IUnknown**)&p, lp, *piid));
}
return *this;
}
};
构造函数可以从IUnknown参数进行查询,找到想要的接口。

增加了一个拷贝赋值函数和一个从IUnknown接口查询获取接口的赋值函数。红色的函数和CComPtr<T>完全一样,不知道为什么要定义?

下面是如何使用的例子:
CComPtr<IUnknown> punk = /* Init to some IUnknown* */ ;
CComQIPtr<INamedObject> pno = punk; // Calls punk->QI // (IID_INamedObject, ...)

一般情况下,我们应该总是用更高级的CComQIPtr类。

 

原创粉丝点击