关于 COM 指针·3则

最近在玩 MSHTML 和 MSXML,被一堆 COM 接口搞的晕头转向。

以前也玩过一些,但基本都是照抄别人的代码,也没注意到 IDispatch * 和 IDispatchPtr 有啥区别,就模糊的认为可能是同一个东东的两种写法罢……

这次趁机仔细研究了一番,转载3篇 COM 智能指针的详细的介绍,以备不时之需

 ATL智能指针类

ATL中包含的智能指针包括:CAutoPtr、CAutoPtrArray和CAutoPtrList, CComPtr和CComQIPtr

一、CAutoPtr、CAutoPtrArray和CAutoPtrList

CAutoPtr是普通智能指针类,CAutoPtrArray是智能指针数组,两者都在atlbase.h中声明;CAutoPtrList是智能指针列表,在atlcoll.h中声明。

二、CComPtr和CComQIPtr

CComPtr和CComQIPtr都是用来管理COM接口指针的类,CComQIPtr可完全取代CComPtr,两者的区别在于:CComQIPtr实现了运算符的重载功能,它可以自动调用QueryInterface()函数,CComQIPtr 唯一的缺点是不能定义 IUnknown * 指针。CComQIPtr<IUnknown > spUn是错误的。

1.声明和定义:

声明和定义一个CComQIPtr指针ISample。

CComQIPtr<ISample> spSample(IOtherInterface); 等价于

CComQIPtr<ISample> spSample =IOtherInterface;/*运算符重载*/  等价于

CComQIPtr<ISample> spSample;

IOtherInterface->QueryInterface(IID_ISample, &spSample);

2.函数调用:

智能指针调用函数分智能指针函数调用和智能指针内部接口指针函数调用,如上面声明的spSample,

spSample.CoCreateInstance(),spSample.QueryInterface等价于API函数调用::CoCreateInstance()和::QueryInterface();

spSample.QueryInterface()等价于内部接口指针函数spSample->QueryInterface()。

spSample->AddRef()正确,但spSample->Release()是错误的,因为它调用的是内部接口函数,并不能完全释放智能指针,而后还会再调用一次Release()释放指针。

3.智能指针的使用

当对象的生命周期很长要小心使用;
当对象的生命周期不确定时不要使用;
千万不要使用全局的智能指针。

一个智能指针使用的问题:

MyFunc()
{
   HRESULT hr;
   CoInitialize(NULL);

   {

   CComPtr<ITestCOMPtr> spTestCOMPtr;
   hr = spTestCOMPtr.CreateInstance(CLSID_TESTCOMPtr);
   if(FAILED(hr))
   {
       returnl_hr;
   }
   //dosomethingof ITestCOMPtr

   }
   CoUninitialize();

}

如果没有红色括号标志智能指针的生命周期,spTestCOMPtr将在程序结束时,也就是CoUninitialize()结束之后才释放,这时套间已经结束,从而引起程序崩溃。

附: CComPtr 和 CComQIPtr类定义

template
class CComPtr {
public:
       typedef T _PtrClass;
       CComPtr() {p=NULL;}
       CComPtr(T* lp) {
               if ((p = lp) != NULL)
                       p->AddRef();
       }
       CComPtr(const CComPtr & lp) {
               if ((p = lp.p) != NULL)
                       p->AddRef();
       }
       ~CComPtr() {if (p) p->Release();}
       void Release() {if (p) p->Release(); p=NULL;}
       operator T*() {return (T*)p;}
       T& operator*() {_ASSERTE(p!=NULL); return *p; }
       T** operator&() { _ASSERTE(p==NULL); return &p; }
       T* operator->() { _ASSERTE(p!=NULL); return p; }
       T* operator=(T* lp){return (T*)AtlComPtrAssign((IUnknown**)&p, lp);}
       T* operator=(const CComPtr & lp) {
               return (T*)AtlComPtrAssign((IUnknown**)&p, lp.p);
       }
       bool operator!(){return (p == NULL);}
       T* p;
};

template
class CComQIPtr
{
public:
       typedef T _PtrClass;
       CComQIPtr() {p=NULL;}
   CComQIPtr(T* lp) {
               if ((p = lp) != NULL)
                       p->AddRef();
       }
       CComQIPtr(const CComQIPtr& lp) {
               if ((p = lp.p) != NULL)
                       p->AddRef();
       }
       CComQIPtr(IUnknown* lp)        {
               p=NULL;
               if (lp != NULL)
                       lp->QueryInterface(*piid, (void **)&p);
       }
       ~CComQIPtr() {if (p) p->Release();}
       void Release() {if (p) p->Release(); p=NULL;}
       operator T*() {return p;}
       T& operator*() {_ASSERTE(p!=NULL); return *p; }
       T** operator&() { _ASSERTE(p==NULL); return &p; }
       T* operator->() {_ASSERTE(p!=NULL); return p; }
       T* operator=(T* lp){return (T*)AtlComPtrAssign((IUnknown**)&p, lp);}
       T* operator=(const CComQIPtr& lp) {
               return (T*)AtlComPtrAssign((IUnknown**)&p, lp.p);
       }
       T* operator=(IUnknown* lp) {
               return (T*)AtlComQIPtrAssign((IUnknown**)&p, lp, *piid);
       }
       bool operator!(){return (p == NULL);}
       T* p;
};

 //更新 修改上述代码,使之成为独立于ATL环境的纯c++ CComPtr类,主要是改写重载运算符函数operator=,实现如下:

 T* operator=(T* lp){ASSERT(lp!=NULL);if(ISEqual(lp)) return p; p->release();lp->addref();  p=lp; return p;}检测是否为自赋值:BOOL ISEqual(IUnknown* pOther){ASSERT(lp); IUnknown pUnknown==NULL; p ->queryinterface(IID_IUnknown, (void**)&pUnknown);BOOL result=(pUnknown==pOther)?TRUE:FALSE; pUnknown->release; return result;}


 T* operator=(const CComQIPtr& lp) {
               ASSERT(&lp!=NULL);p=(T*)lp;if(p)p->addref();return p;}
 

 T* operator=(IUnknown* lp) {
               AASERT(lp); lp->queryinterface(IID_T,(void**)&p);ASSERT(p);return p);
       } 

_com_ptr_t的使用

对于COM调用,大量的AddRef/Release调用无疑是例行公事和让人生厌。

_com_ptr_t 是vc运行库中提供的固有的封装COM接口的智能指针,相比CComPtr/CComQIPtr缺少移植性,但是支持不是ATL实现部分的的异常和操作。

以下是DirectShow中播放一个文件的代码:

    IGraphBuilder *pGraph;
    IMediaControl *pMediaControl;
    IMediaEvent   *pEvent;
   
    // Create the filter graph manager and query for interfaces.
    CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
                        IID_IGraphBuilder, (void **)&pGraph);
    pGraph->QueryInterface(IID_IMediaControl, (void **)&pMediaControl);
    pGraph->QueryInterface(IID_IMediaEvent, (void **)&pEvent);

    // Build the graph. IMPORTANT: Change string to a file on your system.
    pGraph->RenderFile(L"C://Example.avi", NULL);

    // Run the graph.
    pMediaControl->Run();

    // Wait for completion.
    long evCode;
    pEvent->WaitForCompletion(INFINITE, &evCode);

    // Clean up.
    pMediaControl->Release();
    pEvent->Release();
    pGraph->Release();


使用com_ptr_t后简单了很多:

//这部分可以放到头文件中

#include
_COM_SMARTPTR_TYPEDEF( IGraphBuilder , __uuidof(IGraphBuilder) );
_COM_SMARTPTR_TYPEDEF( IMediaControl , __uuidof(IMediaControl) );
_COM_SMARTPTR_TYPEDEF( IMediaEvent , __uuidof(IMediaEvent) );

...

    IGraphBuilderPtr pGraph( CLSID_FilterGraph ) ;//调用CreateInstance
    IMediaControlPtr pMediaControl = pGraph;//这里会自动调用QueryInterface  
    IMediaEventPtr pEvent = pGraph;//这里会自动调用QueryInterface
  
  pGraph->RenderFile( L"L"C://Example.avi", NULL); 
  

    // Run the graph.
    pMediaControl->Run();

    // Wait for completion.
    long evCode;
    pEvent->WaitForCompletion(INFINITE, &evCode);
//不需要调用Release()

假如我们要复制一个接口,常常要

pG1 = pG2;

pG1->AddRef();

...

pG1->Release();

对于_com_ptr_t , 只要pG1 = pG2;就可以了

注意以下的代码:

IGraphBuilderPtr pGraph ;

pGraph.CreateInstance( CLSID_FilterGraph );

...

CoUninitialize();

因为 pGraph 会在超出作用域时析构,也就是在CoUninitialize()之后调用 IUnkown::Release() ,这样会引起问题,可以这样解决:

pGraph.Release();  CoUninitialize();

或者

pGraph = NULL;  CoUninitialize(); //这样会隐含的调用_com_ptr_t::Release()

绝对不可以调用pGraph->Release();  因为这是调用IUnkown::Release()然后再调用一次pGraph.Release();  将会两次释放接口指针。

 

 从 VC7 的 CHtmlView 不能正常退出谈 CComPtr 使用中的一个误区

 
响马< lion@public1.ptt.js.cn>
 
一、错误再现
 
在 VC7 中新建一个 MDI 的 MFC Application,命名为MyHtml, 选择使用 CHtmlView。
 
建立两个 html 文件:
 
home.htm
<head>
<frameset rows="*,30">
   <frame src="test.htm">
   <frame src="about:blank">
</frameset>
</html>


test.htm
<html>
<head>
<script language="JavaScript"><!--
function FreshNew()
{
    window.alert("I'am here.");
    setTimeout('FreshNew();',2000);
}

setTimeout('FreshNew();',2000);
// --></script>
</head>
</html>

修改 CMyHtmlView 的 OnInitialUpdate()

void CMyHtmlView::OnInitialUpdate()
{
   CHtmlView::OnInitialUpdate();
   Navigate2(_T(" http://./home.htm"));
}

编译并运行这个程序,在子窗口打开后将其关闭。你会发现浏览器控件还在运行。
 
二、错误分析
 
在 VC7 中,MFC 在很大程度上使用了 ATL,CHtmlView 也不例外,在 CHtmlView 中,访问 COM 指针的代码被修改为使用 ATL 的 CComPtr。CComPtr 是一个对 COM 指针进行包装的 ATL 模版,它实现了引用时自动 AddRef 和退出时自动 Release 这些以前很烦琐的操作。而由其发展出来的 CComQIPtr 则更将 QueryInterface 包装成 "=" 运算符,更加方便使用。对于这两个模版的详细介绍,不在本文的探讨范围,我只能假设您已经基本了解并已经用过这两个模版。
 
我们再来看看 VC7 的 CHtmlView 对 CComPtr 的使用方法。在函数 OnFilePrint 中,CHtmlView 的代码是这样的:
 
void CHtmlView::OnFilePrint()
{
  // get the HTMLDocument
  if (m_pBrowserApp != NULL)
 {
   CComPtr<IDispatch> spDisp = GetHtmlDocument();
   if (spDisp != NULL)
  {
   // the control will handle all printing UI
   CComQIPtr<IOleCommandTarget> spTarget = spDisp;
    if (spTarget != NULL)
    spTarget->Exec(NULL, OLECMDID_PRINT, 0, NULL, NULL);
  }
 }
}
在我所标记的一行中我们看到这样的代码:
CComPtr<IDispatch> spDisp = GetHtmlDocument();
 
而 GetHtmlDocument 的实现是什么样的呢?我们再来看看:
 
LPDISPATCH CHtmlView::GetHtmlDocument() const
{
 ASSERT(m_pBrowserApp != NULL);
 LPDISPATCH result;
  m_pBrowserApp->get_Document(&result);
  return result;
}
可以知道,GetHtmlDocument 返回的是 get_Document 所输出的一个接口指针,而我们知道,对于 COM 指针的一个使用原则是输出参数时进行引用计数,也就是说我们所获得的这个 result 在 get_Document 内部已经对其进行了 AddRef 调用,函数的调用者在不再需要这个指针的时候必须自行对指针进行 Release。
 
继续,我们再回头看 OnFilePrint 的代码,在代码中使用了 CComPtr 重载过的 "=" 运算符将函数的返回指针赋值给 spDisp。我们已经知道 CComPtr 在函数退出的时候会自动对其所包装的指针进行 Release,一切看起来都是正常而且天体无缝的。
 
那么到底错在哪里呢?恰恰就错在了这个 "=" 上面。
 
依照 COM 指针的引用时计数的原则,CComPtr 在实现的时候实现了自动化的引用计数。即在任何 "=" 操作的时候 AddRef,而在无效时 Release。我们来看看 "=" 运算符的具体实现代码是什么样的:
 
ATLINLINE ATLAPI_(IUnknown*) AtlComPtrAssign(IUnknown** pp, IUnknown* lp)
{
  if (lp != NULL)
   lp->AddRef();
  if (*pp)
  (*pp)->Release();
 *pp = lp;
  return lp;
}
从这段代码可以知道,CComPtr 在拿到指针后,并不是直接将其保存到自己的指针里面,而是先对拿到的指针进行 AddRef,保证引用计数,而后才执行 *pp = lp。
 
这样以来,我们将三部分代码合并起来就成了这样:
 
void CHtmlView::OnFilePrint()
{
 LPDISPATCH result; // 函数 GetHtmlDocument
 m_pBrowserApp->get_Document(&result); // 函数 GetHtmlDocument
 IDispatch* spDisp;
 
 result->AddRef(); // CComPtr 自动完成
 spDisp = result; // CComPtr 自动完成
 
 .......
 
 spDisp->Release(); // CComPtr 自动完成
}
 
能够看出其中的问题吗?对了,result 并没有被释放。问题出在函数输出的并不是一个引用计数完整的 COM 指针,而 CComPtr 并不知道,从而导致了这个指针最终被丢失。而 COM 对象也因为引用计数并没有回归为零而不敢清除自己,最终导致了 CHtmlView 不能正常退出。
 
三、修改
 
通过对上面代码的分析,我们已经清楚了解了 CHtmlView 错误的原因,下面我们就来试图对 CHtmlView 进行修正。
 
1.将 PROGRAM FILES/MICROSOFT VISUAL STUDIO .NET/Vc7/atlmfc/src/mfc 目录中的 viewhtml.cpp 复制到你自己的项目目录,并将其加入到自己的项目中。
2.打开 viewhtml.cpp, 寻找 GetHtmlDocument。
3.将所有的直接将 GetHtmlDocument 函数返回赋值给 CComPtr 指针的语句修改为使用 CComPtr 的 Attach。以 OnFilePrint 为例,代码将修改为下面的样子:
 
void CHtmlView::OnFilePrint()
{
  // get the HTMLDocument
  if (m_pBrowserApp != NULL)
 {
   CComPtr<IDispatch> spDisp;
 
  spDisp.Attach(GetHtmlDocument());
   if (spDisp != NULL)
  {
   // the control will handle all printing UI
   CComQIPtr<IOleCommandTarget> spTarget = spDisp;
    if (spTarget != NULL)
    spTarget->Exec(NULL, OLECMDID_PRINT, 0, NULL, NULL);
  }
 }
}
 
重新编译你的程序,再用最开始我提到的 html 进行测试,你会发现一切都正常了。看起来麻烦一些,但是是正确的。
 
四、结论
 
通过上面分析纠错,我们可以知道,CComPtr 并不是一把万能钥匙,而对 COM 指针的使用也远没有因为 ATL 的出现而变得通俗起来。如果具体到这个例子,我们可以得到一个结论:
 
任何时候不要将函数的返回指针赋值给一个 CComPtr。
  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值