牵着老婆满街逛

严以律己,宽以待人. 三思而后行.
GMail/GTalk: yanglinbo#google.com;
MSN/Email: tx7do#yahoo.com.cn;
QQ: 3 0 3 3 9 6 9 2 0 .

引用计数我不怕之智能指针

转载自:http://www.alisdn.com/wordpress/?p=415
作者:陈海



前言

使用引记数,就算是再历害的高手也难免会出错。而一但出错了,之后再去查问题可就相当的困难了。正如我曾经看到,有一段代码是这样的:

m_spView->Release();
m_spView
->Release();
m_spView
->Release();

 

看到这段代码,就知道引用计数出问题了。他想通过这种方式,把多出来的计数Release掉。但这么做能解决问题吗?答案是不能,这样的代码还可能造成严重的稳定性问题。解决引用计数问题,除了要了解引用计数规则外,我们还提昌要用智能指针。智能能帮助我们很好的处理引用计数问题。

 

智能指针的差异

在用VC开发应用程序时,有两个引用计数类可供我们使用。_com_ptr_t与CComPtr,它们都能很好的帮助我们解决引用计数处理。但这两个类还是有一点小小的区别,有的时候这一点区别也是致命的,因此我们必须清楚它们的差别。下面我罗列了它们之间的差别:

  1. CComPtr的&运算符不会释放原指针,而_com_ptr_t会释放原指针。
  2. CComPtr对AddRef与Release做了限制,也就是不充许调用这两个方法,而_com_ptr_t并没有限制。
  3. 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;
}

这段代码能导致两个问题

  1. 引用计数泄漏
  2. m_Views中的指针变成空了

泄漏是由于Detach返回IView*,并不会Release,而spView又会再调用一次AddRef。智能指针的Detach是会把自己设成空的,否则还叫什么Detach。

使用智能指针,是解决引用计数问题最好的办法。不要因为用智能指针,会引入新的问题,而放弃使用它。只要花心思搞清楚智能指针的不同点,使用时注意一些细节问题,使用起来应该会变的非常轻松。


posted on 2011-01-18 16:14 杨粼波 阅读(1501) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理