CPP魔幻游乐场

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

今天遇到一个内存泄露的问题。是师兄检测出来的。Variant类型在使用后要Clear否则会造成内存泄露,为什么呢?

Google一下找到下面一篇文章,主要介绍了Com的内存泄露,中间有对Variant的一些解释吧。

1. 引用计数泄漏
由于C++的一些对象生命周期难以管理,在COM中加入了引用计数,用来解决这个问题,引用计数是COM最重要的特性之一。但讽刺的是,虽然很多COM组件是用C++写的,但是在所有编程语言中,C++使用COM是最麻烦的,而且最容易产生引用计数的泄漏,从而最终造成内存泄漏。

引用计数泄漏一旦产生,比new了之后忘了delete还难以定位,因为一个对象可能被很多地方使用,根本就不知道到底是那个使用的地方在AddRef之后没有Release,只能知道对象泄漏了,但不知道是哪里导致泄漏。

因此在使用COM的时候,尽量使用智能指针,而且最好是所有使用COM的地方全部使用智能指针,这样能在很大成都上防止产生COM对象的引用计数泄漏。

然而使用了智能指针一定能解决问题吗?也不一定,至少有以下两种情况,使用了智能指针仍然会产生引用计数泄漏,而且比上面的情况更加难以找到原因:

对象循环引用导致引用计数泄漏
如果有两个对象A和B,在A中使用了B,同时在B中使用了A,B对象中有一个智能指针指向A,A对象中有一个智能指针指向B,这时候就会产生循环引用导致。因为A对象能被析构的前提是B对象先析构,而B对象析构的前提同样是A对象先析构,这就成了一个死局,两个对象都无法析构,A和B都产生了内存泄漏。

要想解决这种问题,一种方法是把这个循环的链打断,在某个合适的地方把某一个对象中的智能指针显示地把智能指针赋值为NULL,这样循环的条件被打破,两个对象就都可以析构了。另一种方法是使用弱引用,自动完成。但弱引用相对有点复杂,在本文中不作介绍,读者可以自行在网上搜索相关资料。

不正确的智能指针使用方法导致引用计数泄漏
有时候使用了智能指针,并且也没有循环引用,但是对象仍然有引用计数泄漏,是不是很困惑?对于这种情况,有可能是因为使用智能指针不正确引起的。

我们有时候会这样使用智能指针:

pUnknown->QueryInterface(__uuidof(IMyInterface), & spMyInterface);

spMyInterface变量是一个智能指针,在大多数情况下这样使用不会有任何问题,尤其是使用了微软的_com_ptr_t或CComPtr等。但如果我们使用一些第三方的智能指针,例如Boost里的intrusive_ptr。我们知道,在_com_ptr_t和CComPtr中,重载了&操作符,在&操作符中,会先释放目前的引用计数,然后返回一个指向指针的指针。这样带来的好处是使用&操作符的时候,都会先释放当前的对象,不会造成内存泄漏,坏处是这个智能指针可能无法放到一些STL的容器中,因为有一些STL的容器,在移动容器中的数据单元时,会用到&操作符,这样的结果是一旦移动,对象就被释放了。Boost的智能指针,例如intrusive_ptr是没有重载&操作符的,因此可以放心地在STL容器中使用,但如果写类似于QueryInterface(__uuidof(IMyInterface), & spMyInterface)就要特别小心了,例如像下面的连续两次QueryInterface用法,就会产生引用计数泄漏:

boost::intrusive_ptr    spMyInterface;

pUnknown->QueryInterface(__uuidof(IMyInterface), & spMyInterface);

pUnknown2->QueryInterface(__uuidof(IMyInterface), & spMyInterface);

因为第二次调用QueryInterface之前,spMyInterface并没有Release,因此就有引用计数泄漏了。如果在某个循环中这样使用,则问题会更加严重。

要解决这类问题,有两种方法。第一是同一个变量永远不要多次使用,在变量声明周期中类似的操作只使用一次,如果有多次使用的需求,用别的变量来实现。另一种方法是每次使用之前,都先显示地把变量清空,释放引用计数,例如先spMyInterface,再调用QueryInterface。

2. 字符串(BSTR)泄漏
字符串泄漏容易被忽视,而且COM中使用的BSTR字符串,并不是用new或者malloc等来分配的,而是用类似于::SysAllocString等API来分配的,要检测是否有泄漏更加困难。

我们一般在调用某个类似于这样的函数GetString([out] BSTR * str)的返回BSTR的指针的函数后,需要调用::SysFreeString把字符串释放掉,和new/delete、malloc/free、AddRef/Release类似。如果忘记调用::SysFreeString,就会产生内存泄漏。

一般我们使用字符串类来自动释放,例如使用CComBSTR,就可以这样写:

CComBSTR str;

GetString(&str);

str在析构的时候会自动调用::SysFreeString把字符串释放掉。

但如果看一下CComBSTR类的实现,我们会发现,这个类和Boost的intrusive_ptr类似,是没有重载&操作符的,这就带来和intrusive_ptr一样的问题,如果连续两次以上使用这个字符串,就会产生内存泄漏,例如字符串类我们有时候会这样用:

CComBSTR str;

for(int I = 0;I < 100;I ++)

{

    pInterface->GetString(&str);

    // do something

}

    这样的后果是每调用一次,就会 产生一个内存泄漏。因此一定要保证在字符串对象的生命周期中,只被使用一次,代码改为这样就不会有问题:

for(int I = 0;I < 100;I ++)

{

    CComBSTR str;

    pInterface->GetString(&str);

    // do something

}

    另外还有一种方法是使用_bstr_t的GetAddress函数,这个函数返回的是 BSTR *,每次调用里面都会先释放当前的字符串,因此使用 _bstr_t属于比较保险并且方便的选择。不过遗憾的是在VC6中这个类并没有实现GetAddress函数,用起来可能会影响代码的移植性。

3. VARIANT泄漏
这个情况和字符串泄漏类似,变量不再使用之后,应该调用::VariantClear来释放内存,否则就可能会产生内存泄漏。

解决方法也和字符串类似,可以使用VARIANT的CComVariant,但这个类和CComBSTR也有一样的问题,因此使用的时候也一样要注意,必须保证每个变量的生命周期中只使用一次。

另外有一个和_bstr_t类似的类:_variant_t,里面有一个GetAddress函数可以比较方便和安全的使用。具体的细节请参考这几个类的实现代码,这里不多做介绍。

4. 总结
对于本文中提到的几种内存泄漏的原因,根源在于使用一个第三方的类的时候,其实并没有真正理解这些类的实现原理,我们以为正确地使用了,但其实用法并不对。因此在使用一个不是自己写的第三方类的时候,不能直接拿过来就用,而是应该花一点时间去仔细了解一下这个类,知道怎样使用是合理的,而怎样使用是不正确的,把所有的隐患在编码之前就解决掉,整个项目开发过程就会更加顺利,产品质量就会更高。

关于第三方类库的使用,在本人另一篇博文《编码原则十日谈》中有提及,这里给出地址,供读者参考。

http://blog.csdn.net/cplusplus_zk/archive/2008/11/29/3407731.aspx


文章出处:飞诺网(www.firnow.com):http://dev.firnow.com/course/3_program/c++/cppjs/20091129/183786.html

 

posted on 2010-06-22 17:19  laurel  阅读(1776)  评论(0编辑  收藏  举报