前言
使用引记数,就算是再历害的高手也难免会出错。而一但出错了,之后再去查问题可就相当的困难了。正如我曾经看到,有一段代码是这样的:
m_spView->Release();
m_spView->Release();
m_spView->Release();
看到这段代码,就知道引用计数出问题了。他想通过这种方式,把多出来的计数Release掉。但这么做能解决问题吗?答案是不能,这样的代码还可能造成严重的稳定性问题。解决引用计数问题,除了要了解引用计数规则外,我们还提昌要用智能指针。智能能帮助我们很好的处理引用计数问题。
智能指针的差异
在用VC开发应用程序时,有两个引用计数类可供我们使用。_com_ptr_t与CComPtr,它们都能很好的帮助我们解决引用计数处理。但这两个类还是有一点小小的区别,有的时候这一点区别也是致命的,因此我们必须清楚它们的差别。下面我罗列了它们之间的差别:
- CComPtr的&运算符不会释放原指针,而_com_ptr_t会释放原指针。
- CComPtr对AddRef与Release做了限制,也就是不充许调用这两个方法,而_com_ptr_t并没有限制。
- CComPtr只能接受模版参数指定的指针,_com_ptr_t可以接受任何接口指针,并自动调用QueryInterface得到模板参数指定的指针类型。
这些区别,导致了有些代码不能同时应用于两个智能指针。
&运算符差异带来的风险
HRESULT hr GetView(int i, /*out*/IView** ppView)
{
*ppView = m_Views[i];
(*ppView)->AddRef();
return S_OK;
}…
}
CComPtr<IView> spView;
for (int i = 0; i < 10; i++)
{
GetView(i, &spView);
spView->
以上代码会导致引用计数出错,前面的9个View的引用计数并没有Release。CComPtr<IView>的&运算符,会返回IView**也就是CComPtr内部成员的地址,但它不释放原来的指针。而GetView又会修改指针,直接把原来的指针抛弃了。
这个代码可以这样改:
for (int i = 0; i < 10; i++)
{
CComPtr<IView> spView;
GetView(i, &spView);
spView->…
}
把指针作为循环的局部变量,这样每次循环退出前spView都会被析构,最终调用Release。当然还能这样改:
COM_SMARTPTR_TYPEDEF(IView, __uuidof(IView));
IView Ptr spView;
for (int i = 0; i < 10; i++)
{
GetView(i, &spView);
spView->…
}
_com_ptr_t的&运算符会帮助我们把原来的指针Release掉,所以我们就不必担心引用计数没有释放。
禁用AddRef与Release
然我们使用的智能指针,就不要再去调AddRef或Release了。如果再去手工调用它们,就失去了智能指针的好处。CComPtr有一个非常巧妙的方法,禁止调用这两个方法。它声明了一个类_NoAddRefReleaseOnCComPtr,它的定义如下:
template <class T>
class _NoAddRefReleaseOnCComPtr : public T
{
private:
STDMETHOD_(ULONG, AddRef)()=0;
STDMETHOD_(ULONG, Release)()=0;
};
我们看到,里面就定义了两个私有函数。AddRef与Release,它们重写了IUnknown的这两个方法,并且继承自模板T。再来看段代码:
_NoAddRefReleaseOnCComPtr<T>* operator->() const
throw()
{
ATLASSERT(p!=NULL);
return (_NoAddRefReleaseOnCComPtr<T>*)p;
}
我们看到的是CComPtr的”->”运算符,它将内部的指针强制转换成_NoAddRefReleaseOnCComPtr<T>*。其中T是CComPtr的模板参数,也就是接口指针类型。可以看出_NoAddRefReleaseOnCComPtr<T>继承自接口类型,因此通过_NoAddRefReleaseOnCComPtr<T>*可以调用T的所有函数。前面我们看到NoAddRefReleaseOnCComPtr的两个私用函数,AddRef与Release,如果有谁想调用就会报编译错误。
自动QueryInterface
_com_ptr_t有多个=运算符版本,代码如下:
template<typename _IIID> class _com_ptr_t
{
public:
typedef _IIID ThisIIID;
typedef
typename _IIID::Interface Interface;
// Queries for interface.
template<typename _InterfaceType> _com_ptr_t& operator=(_InterfaceType* p)
{
HRESULT hr = _QueryInterface(p);
if (FAILED(hr) && (hr != E_NOINTERFACE)) {
_com_issue_error(hr);
}
return *this;
}
// Saves the interface.
template<> _com_ptr_t& operator=(Interface* pInterface) throw()
{
if (m_pInterface != pInterface) {
Interface* pOldInterface = m_pInterface;
m_pInterface = pInterface;
_AddRef();
if (pOldInterface != NULL) {
pOldInterface->Release();
}
}
return *this;
}
其中
template<typename _InterfaceType> _com_ptr_t& operator=(_InterfaceType* p)是一个模板函数,接受任意类型的指针,函数内部会调用传入参数”p”的QueryInterface。
template<> _com_ptr_t& operator=(Interface* pInterface) throw()是模板函数的一个偏特化版本,接受_com_ptr_t模板参数中指定的指针类型。当传入的接口指针类型,与类模板指定的类型一样时这个函数会被调用。它不需要做QueryInterface的调用,只是简单的AddRef;
综上所述,两个智能指针在同一份代码里混用,很可能导致不良后果。所以我认为,最好不要在同一份代码里混用。而这两个指针,我很喜欢_com_ptr_t,它在许多方面明显优于CComPtr。
Attach与Detach
使用了智能指针,也并不是高枕无忧了。它还是给我们带来了一些新的问题。
有一些第三方类库设计的不合理,它在函数的返回值里返回接口指针。如下代码就导会导致引用计数泄漏:
IView* GetView(int nIndex)
{
IView* pView = m_Views[nIndex];
pView->AddRef();
return pView;
}
IViewPtr spView = GetView(0);
以上代码,注意调用GetView的地方。IViewPtr是智能指针,它的=运算符是会再调用AddRef而GetView里已经调了一次AddRef了,这里多了一次AddRef。别问我GetView中为什么要AddRef,这是引用计数规则,不清楚请看《引用计数我不怕之引用计数规则》。
解决这个问题的方法就是用Attach函数
IViewPtr spView;
spView.
Attach(GetView(0));
也许是有人写了Attach,而其它人不明白Attach的意思,结果写出了这样的代码。
void SetView(IView* pView)
{
m_spView.Attach(pView);
}
根据引用计数规则,将指针保存为副本,必须AddRef。但是这个例子里没有这么干,结果m_spView变成了野指针。
前面我们看到的GetView很简单,但是下面我们要做一别的事情,于是要用智能指针。
HRESULT hr GetView(int nIndex, IView** ppView)
{
IViewPtr spView = m_Views[nIndex];
if (spView->IsVisable() != S_OK)
return E_FAILD;
*ppView = spView;
return S_OK;
}
表面看来没什么问题,但在函数返回后,智能指针又会调用一次Release。要解决这个问题,可以调用Detach。
HRESULT hr GetView(int nIndex, IView** ppView)
{
IViewPtr spView = m_Views[nIndex];
if (spView->IsVisable() != S_OK)
return E_FAILD;
*ppView = spView.
Detach();
return S_OK;
}
Detach还是会被乱用,看到这样的代码还真是哭笑不得。
HRESULT hr ChangeView(int nIndex)
{
IViewPtr spView = m_Views[nIndex].Detach();
spView->Change();
return S_OK;
}
这段代码能导致两个问题
- 引用计数泄漏
- m_Views中的指针变成空了
泄漏是由于Detach返回IView*,并不会Release,而spView又会再调用一次AddRef。智能指针的Detach是会把自己设成空的,否则还叫什么Detach。
使用智能指针,是解决引用计数问题最好的办法。不要因为用智能指针,会引入新的问题,而放弃使用它。只要花心思搞清楚智能指针的不同点,使用时注意一些细节问题,使用起来应该会变的非常轻松。