天天看點

Variant 與 記憶體洩露

http://blog.chinaunix.net/uid-10386087-id-2959221.html

今天遇到一個記憶體洩露的問題。是師兄檢測出來的。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;

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一樣的問題,如果連續兩次以上使用這個字元串,就會産生記憶體洩漏,例如字元串類我們有時候會這樣用:

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

{

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. 總結

對于本文中提到的幾種記憶體洩漏的原因,根源在于使用一個第三方的類的時候,其實并沒有真正了解這些類的實作原理,我們以為正确 地使用了,但其實用法并不對。是以在使用一個不是自己寫的第三方類的時候,不能直接拿過來就用,而是應該花一點時間去仔細了解一下這個類,知道怎樣使用是 合理的,而怎樣使用是不正确的,把所有的隐患在編碼之前就解決掉,整個項目開發過程就會更加順利,産品品質就會更高。

關于第三方類庫的使用,在本人另一篇博文《編碼原則十日談》中有提及,這裡給出位址,供讀者參考。

繼續閱讀