com point

 

通过引用计数确实可以很合理的管理组件的生命周期,但也严格要求开发人员遵循下面这三条简单规则【1】:

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

2.使用完接口之后调用Release。在使用完某个接口之后应调用此接口的Release函数。

3.在赋值之后调用AddRef。将一个接口指针赋值给另外一个接口指针时,应调用AddRef。换句话说,在建立接口的另外一个引用之后应增加相应组件的引用计数。

根据如上三条原则我们需要编写出了形如下例的代码:

void SomeApp( IHello * pHello )
{

    IHello* pCopy = pHello;
    pCopy->AddRef();
    OtherApp();
    pCopy->Hello();
    pCopy->Release();
}

这三条规则看似简单,但是程序员若在某一时刻遗漏掉其中的某条规则,则会使得引用计数陷入混乱。出错后,最乐观的情况是程序由于访问了已经被释放的资源,而直接崩溃。如果运气不是特别好,那么资源泄露则悄无声息的发生了。

可以看出由于引用计数的引入,COM组件的生命周期可以自行管理,但同时也使得COM的使用变得非常危险。因为使用过程中需要每一个使用者都要严格并且正确的调用AddRef()和Release(),一旦出现问题,就会造成对象不能被正常释放,或者对象被重复删除,造成程序崩溃。所以使用COM接口,必须小心翼翼才行。

我们试着用智能指针改写上述代码。这里以CComPtr为例,更多关于它的介绍我们放到后续条款中。上述函数将会变成如下这种形式:

view plaincopy to clipboardprint?void SomeApp( IHello * pHello ) 
void SomeApp( IHello * pHello )
{
    CComPtr<IHello> spHello= pHello;
    OtherApp();
    spHello->Hello();
}

 

按照功能和实现原理选择合适的智能指针
智能指针种类繁多,从实现原理上可以划分为“基于所有权传递”和“基于引用计数”两大类。C++中内置的std::auto_ptr则为“基于所有权传递”的智能指针。而Boost库中的shared_ptr则是“基于引用技术”的智能指针(目前已经成为C++0x【6】的一部分)。

如“基于所有权传递”的智能指针会在将自己赋值给其他智能指针或者普通指针后将自己置空。这样资源对象的权限被“移交”出去,使得每个资源在一个时候仅仅有一个拥有者持有其访问权限。如std::auto_ptr便是这样的指针:

auto_ptr<int> ap1(new int(0));
auto_ptr<int> ap2 = ap1;
cout<<*ap1; //错误,此时ap1只剩一个null指针在手了 
 

这种智能指针简单,且能尽早的发现错误(不像引用计数错误后导致难以查找的问题。)智能指针对资源的唯一所有权,使得其很明确的知道什么时候需要对其释放。

而缺点也是很明显的:资源共享不方便。且无法将这类指针放置于STL这样的标准类模版中【7】。因为STL参数传递都采用的是值传递,而非引用传递。试想一下,若放置一个智能指针在容器中,而后只是通过容器访问了一下此指针,容器中的指针就指向NULL了,情况是多么糟糕。

而“基于引用计数”的智能指针则会在智能指针内部维持一个引用计数。当产生一份智能指针的拷贝(如将一个智能指针赋值到另一个智能指针)则引用计数递增一次,若智能指针出栈,则析构函数会让引用计数递减一次。若当引用计数为0则表明没有任何智能指针再指向这个资源对象了,此时智能指针会对资源进行销毁。

COM的智能指针都采用这种方式实现,因为COM本生就采用引用计数,而且需求上也要求COM能被多个指针访问,这些指针都应当有COM组件的所有权。如下:

  
CComPtr<IX> spIX = NULL;
HRESULT hr = spIX.CoCreateInstance(CLSID_MYCOMPONENT);
If(SUCCEEDED(hr))
{
    spIX->Fx();
    CComPtr<IX> spIX2 = spIX;//赋值操作会使得原来的引用计数递增。
    spIX2->Fx();
    spIX->Fx();      //赋值运算后,原来的智能指针还是能继续使用。
}//智能指针析构的时候递减其引用计数,并决定是否销毁其所拥有的资源
 
 

而按照功能上划分,则可以用“COM的智能指针”、“内存管理智能指针”、“IO智能指针”等将智能指针划分为不同类别。

如shared_ptr 和 CComPtr虽然从实现原理上说都是“基于引用技术”的。但却会完成不同的功能,如:shard_ptr在析构时候发现引用计数为0时,会delete掉指针所指的那段内存。而CComPtr则是在析构时简单的调用COM接口的Release()函数。引用计数和COM组件的销毁工作都是由组件自身完成的。明白了这一点,你可能就不会轻易的将一个智能指针简单的用在一个COM接口上了。

 

 

而CComPtr是ATL为了解决COM引用计数带来的问题,提供的一个类模版。因此你可能明白了。智能指针需要通过模版来实现(大多数智能指针考虑到通用性都是这么设计,也有少数例外)。因此你似乎明白了“智能指针”只是这种特定模版的一个较为通俗易懂的别名。它不会让人有种,因为使用了模版这类高级的C++编程技巧而产生高不可攀的感觉。同时在使用和它的行为上也更加类似于一个接口指针。

ATL除了提供CComPtr还提供了一个名为CComQiPtr的智能指针。两个智能指针的模版类用于实现对COM接口引用计数的自动管理,且都在<atlbase.h>中声明。

这两个模版类都继承自CComPtrBase,不同之处在于CComQiPtr能在必要的时候自动的对所需接口进行查询(如:对与此智能指针参数化类型不同的指针赋值时,会自动查询是否有所需的接口)。CComPtrBase类封装了CComPtr和CComQiPtr中公共的大多数函数,从而实现代码的复用。下图显示了这3个模版类的继承关系:

  

值得注意的是如果你的VC编译器版本过于老旧(比如vc6.0)则在ATL中无法使用到CComQiPtr,也看不到CComPtrBase这个基类。仅仅有CComPtr孤零零的呆在<atlbase.h>这个头文件中。这是因为在早期的ATL中,设计者只是设计了CComPtr这么一个模版类。而后期为了加入新的功能才将CComPtr和CComQiPtr的代码抽出来放入到CComPtrBase这个基类中去。

因此本文仅讨论CComPtr以及其基类CComPtrBase,而不涉及关于CComQiPtr的内容(有兴趣的读者可以查阅MSDN或阅读ATL源码了解其中的细节)。在CComPtr中除了构造函数和赋值运算符之外的大多数函数都源自于CComPtrBase,为了方便阅读和理解,本文会将这两个类中的成员函数放在一起讨论。

让我们再来看一次CComPtr的使用,或许进过一番介绍以后,你会对他内部如何管理引用计数有更加深刻的理解:

以IHello*为例,将程序中所有接口指针类型(除了参数),都使用CComPtr<IHello> 代替即可。即程序中除了参数之外,再也不要使用IHello*,全部以CComPtr<IHello>代替。

如下:

void SomeApp( IHello * pHello )
{
    CComPtr<IHello> pCopy = pHello;
    OtherApp();
    pCopy->Hello();
}
 

最后值得一提的是,虽然CComPtr的用法和普通COM接口指针类似,但是还是要主要如下几个问题:

1. CComPtr已经保证了AddRef和Release的正确调用,所以不需要,也不能够再调用AddRef和Release。

2. 如果要释放一个智能指针,直接给它赋NULL值即可。

3. CComPtr本身析构的时候会释放COM指针。

4. 当对CComPtr使用&运算符(取指针地址)的时候,要确保CComPtr为NUL。(因为通过CComPtr的地址对CComPtr赋值时,不会自动调用AddRef,若不为NULL,则前面的指针不能释放,CComPtr会使用assert报警)

ATL追求的是简洁与高效,因此在解决一个问题之时,CComPtr不会将解决问题的方式做到面面俱到。例如如果用一种方法能够解决这一问题,ATL尽可能不会采用第二种方法(除非这两种方法确实有不同之处)。

这一点从ATL的构造函数可以看出,他只有4个构造函数:1个必要的默认构造函数,1个必要的拷贝构造函数。还有2个为了满足指针语法而存在的转换构造函数。这样“恰当好处”就足够了。至于组件的创建过程,接口的查询过程,ATL认为这些东西都可以通过构造好之后的智能指针来完成。如下:

CComPtr<ICalculator> spCalculator= NULL;
hrRetCode = spCalculator.CoCreateInstance(CLSID_CALCULATOR);
KG_COM_ASSERT_EXIT(hrRetCode);
hrRetCode = spCalculator->Add(1, 2, &nSum);
KG_COM_ASSERT_EXIT(hrRetCode);
 

上述代码中你可能会怀疑智能指针没有满足RAII,但是他确实满足了。RAII并没有要求资源管理对象与资源同声明周期。他只是需要资源在获取之处就与一个资源管理对象绑定起来。ATL认为这样做最够了,而且代码清晰简单,那么他这样做了。

再来看看CComPtr的赋值运算符,他并没有做自动的接口查询工作(需要说明的是,CComPtr在后续版本中为部分赋值运算符加入了此种操作)。ATL认为有了QueryInterface这套接口,则开发人员就能方便的完成接口查询工作了。如下:

view plaincopy to clipboardprint?ICalculator* pCalculator = NULL; 
CComPtr<IUnknown> spIUnknown   = NULL; 
hrRetCode = spIUnknown.CoCreateInstance(CLSID_CALCULATOR); 
KG_COM_ASSERT_EXIT(hrRetCode); 
hrRetCode = spIUnknown.QueryInterface(&pCalculator);//通过此函数查询接口  

 


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值