首页 > 程序开发 > 软件开发 > C语言 >

有效的使用和设计COM智能指针——条款12

2011-09-16

条款12:必要时使用attach() 和 detach()调整引用计数假设我们使用了一个第三方编写的函数,或者它也是由我们编写的,但仅仅是由于起初没有使用智能指针。于是它的实现可能是如下这种样子的:view plaincopy ...

条款12:必要时使用attach() 和 detach()调整引用计数
假设我们使用了一个第三方编写的函数,或者它也是由我们编写的,但仅仅是由于起初没有使用智能指针。于是它的实现可能是如下这种样子的:

view plaincopy to clipboardprint?IView* GetView(int nIndex)
{
IView* pView = m_Views[nIndex];
pView->AddRef(); //引用计数被增加了一次。
return pView;
}
IView* GetView(int nIndex)
{
IView* pView = m_Views[nIndex];
pView->AddRef(); //引用计数被增加了一次。
return pView;
}
这个函数没有问题,简单易懂。在传出参数的时候,还遵守了引用计数的三条规则从而调用了一次AddRef()。但我们如果这样使用它,情况会怎样呢?

view plaincopy to clipboardprint?void UseView(int nIndex)
{
CComPtr<IView> spView = NULL;
spView = GetView(0); //错误的根源在这里
spView->UserIt();
}
void UseView(int nIndex)
{
CComPtr<IView> spView = NULL;
spView = GetView(0); //错误的根源在这里
spView->UserIt();
}
这再次出现了引用计数问题中最糟糕的结果——资源泄漏,且无声无息。

如果你没有发现上述引用计数的错误之处我们来“单步”看一下这一资源泄露问题是如何产生的:

1.首先在UseView中创建了一个智能指针。他指向为NULL;

2.调用GetView(0),他返回一个接口指针,并且增加了组件的引用计数。

3.GetView(0)通过智能指针重载的赋值运算符将接口指针赋值给智能指针,此时赋值运算符会增加一次引用技术。

4.通过智能指针使用接口。

5.智能指针出栈。引用计数递减一次。

认真查看上述过程,你会发现,引用技术增加了两次,而却只递减了一次。因此错误的根源在于智能指针重载的赋值运算符为我们多增加了一次引用计数。于是有人想到用下面这种方法来解决这一问题:

view plaincopy to clipboardprint?void UseView(int nIndex)
{
CComPtr<IView> spView = NULL;
spView = GetView(0); //错误的根源在这里
spView->Release(); //这样解决?很费解。
spView->UserIt();
}
void UseView(int nIndex)
{
CComPtr<IView> spView = NULL;
spView = GetView(0); //错误的根源在这里
spView->Release(); //这样解决?很费解。
spView->UserIt();
}
看到这样的代码你可能会问。Release()操作的理由在哪里? 为什么没有AddRef却单独出现了一个Release()。但如果你知晓这一过程的话,你会发现这样做的理由仅仅是为了抵消赋值运算符带来的一次副作用(多增加了引用计数)。但这样却给人造成理解上的困难,同时对于支持部分资源分配的接口,这里可能带来一些性能上的损失。

正确的解决方式是这样的:

view plaincopy to clipboardprint?void UseView(int nIndex)
{
CComPtr<IView> spView = NULL;
spView.Attach(GetView(0)); //OK,问题解决了。也很容易弄懂其意义。
spView->UserIt();
}
void UseView(int nIndex)
{
CComPtr<IView> spView = NULL;
spView.Attach(GetView(0)); //OK,问题解决了。也很容易弄懂其意义。
spView->UserIt();
}
利用智能指针提供的Attach操作可以将接口指针绑定到智能指针智能指针之上,而不增加其引用计数。再看看函数的运行过程,引用计数的增加和减少是不是平衡了?

或许你会说上面的GetView接口并不是十分符合COM设计的习惯,因为他没有用HRESULT作为返回值从而标识调用的成功与否。于是在你的新版本中你使用了HRESUTL作为返回值,用一个接口指针作为传出参数。最重要的是,你想到了通过智能指针来实现这个函数。这些想法都是很合理的,于是它看上去成了下面这个样子:

view plaincopy to clipboardprint?HRESULT hr GetView(int nIndex, IView** ppView)
{
CComPtr<IView> spView = m_Views[nIndex];
if (FAILED( spView->IsVisable()))
return E_FAILD;
*ppView = spView; //欧~ 这里有出错了
return S_OK;
}
HRESULT hr GetView(int nIndex, IView** ppView)
{
CComPtr<IView> spView = m_Views[nIndex];
if (FAILED( spView->IsVisable()))
return E_FAILD;
*ppView = spView; //欧~ 这里有出错了
return S_OK;
}
这又是一处潜在的让人难以琢磨的错误。如果你不能发现他,那我们来只能“单步”一次了。这可能会让你多动一动脑筋,但总比让程序崩掉或者内存泄漏要值得的多:

1.首先在UseView中创建了一个智能指针。他指向了一个COM接口,接口引用计数增加。

2.调用ISVisable(),并判断是否成功。若失败,则返回,此时智能指针被析构。引用计数递减。若成功,则进行下一步。

3.将智能指针赋值到接口指针,此时会完成一个隐式转换。引用计数不变。

4.函数结束。智能指针被析构。引用计数递减。

由以上步骤可以看出,若这个函数执行失败,那么没有任何问题。引用计数的增减是平衡的。但若这个函数成功呢?你可能要回到条款1,去看一看令人头疼的引用计数规则了:

“在返回之前调用AddRef。对于那些返回接口指针的函数,在返回前应用相应的指针调用AddRef。这些函数包括QueryInterface及CreateInstance。这样当客户从这种函数得到一个接口后,他将无需调用AddRef。”

很显然这里没有这样做。导致的后果是外界可能有一个指针或者引用接收到了此接口指针,而引用计数却没有被增加!

你可能又想到了在其出栈前手动调整计数的方法。请别这样做,那会使得语义不明确。最佳解决的办法是使用detach操作:

view plaincopy to clipboardprint?HRESULT hr GetView(int nIndex, IView** ppView)
{
CComPtr<IView> spView = m_Views[nIndex];
if (FAILED( spView->IsVisable()))
return E_FAILD;
*ppView = spView.Detach(); //在智能指针出栈前,将其与接口指针分离。
return S_OK;
}
HRESULT hr GetView(int nIndex, IView** ppView)
{
CComPtr<IView> spView = m_Views[nIndex];
if (FAILED( spView->IsVisable()))
return E_FAILD;
*ppView = spView.Detach(); //在智能指针出栈前,将其与接口指针分离。
return S_OK;
}

这或许是智能指针最让人苦恼的地方,他只是从一定程度上实现了引用计数的自动化。而却没有完完全全的解决这个问题。问题的根本在于,智能指针想实现引用计数的自动化,同时又需要兼容COM引用技术遗留下来的三条规则。对于这些潜在的问题,程序员或许只能小心翼翼了。

作者“liuchang5的专栏”

相关文章
最新文章
热点推荐