條款10:盡量減少智能指針和接口指針的混用
在開始一節之前,讓我們先來看一個例子:
view plaincopy to clipboardprint?void func(void)
{
ICalculator *pCalculator = NULL;
CComPtr<ICalculator> pCalculator = NULL;
hrRetCode = CoCreateInstance( //在這裡創建COM組件,引用計數為1。
CLSID_CALCULATOR,
NULL,
CLSCTX_INPROC_SERVER,
IID_ICALCULATOR,
(void **)&pCalculator
);
assert(hrRetCode);
spCalculator = pCalculator; //將創建好的組件交給智能指針讓其管理引用計數
//賦值運算符會再次調用AddRef()增加引用計數
spCalculator->DoSomething();
} //調用Release()後引用計數不為0,發生了資源洩漏。
void func(void)
{
ICalculator *pCalculator = NULL;
CComPtr<ICalculator> pCalculator = NULL;
hrRetCode = CoCreateInstance( //在這裡創建COM組件,引用計數為1。
CLSID_CALCULATOR,
NULL,
CLSCTX_INPROC_SERVER,
IID_ICALCULATOR,
(void **)&pCalculator
);
assert(hrRetCode);
spCalculator = pCalculator; //將創建好的組件交給智能指針讓其管理引用計數
//賦值運算符會再次調用AddRef()增加引用計數
spCalculator->DoSomething();
} //調用Release()後引用計數不為0,發生了資源洩漏。
如果不仔細觀察,可能很難發現上述例子中存在一個資源洩漏問題。但細致的分析一下,可以發現上述代碼中CoCreateInstance中會調用一次接口的AddRef()。而在智能指針賦值過程中又調用了一次AddRef()。但智能指針析構時只有一次Release()調用,因此,這個時候COM的引用技術沒有歸零,從而不會析構。但函數結束後,沒有任何指針能夠再次訪問到COM組件。因此它在內存中“游離”了,資源洩漏也就產生了。
問題的關鍵在於接口指針與智能指針混合使用了。而在最後忘記調用接口指針的Release()從而導致了資源洩漏。
可能你會在此函數最末尾補上一句“pCalculator->Release()”,但非常不推薦你這樣做。因為我們引入智能指針的目的就是為了盡可能簡化我們的開發。若是將智能指針與接口指針混合使用,則會要話費更多的時間來思考引用計數問題。
因此最好的解決辦法是將接口指針從函數中完全剔除出來。上述函數完全可以由一下這個例子來完成:
view plaincopy to clipboardprint?void func(void)
{
CComPtr<ICalculator> pCalculator = NULL; //只有智能指針
hrRetCode = pCalculator .CoCreateInstance(CLSID_CALCULATOR,);//引用計數為1
assert(hrRetCode);
spCalculator->DoSomething();
} //調用Release()後引用計數為0,資源正確的被釋放了
void func(void)
{
CComPtr<ICalculator> pCalculator = NULL; //只有智能指針
hrRetCode = pCalculator .CoCreateInstance(CLSID_CALCULATOR,);//引用計數為1
assert(hrRetCode);
spCalculator->DoSomething();
} //調用Release()後引用計數為0,資源正確的被釋放了
混用智能指針會使得代碼引用計數難以琢磨,因此我們不應當在代碼中混用智能指針與接口指針。
但凡是都有例外,接口指針並非沒有用武之地。而且你已經在之前的章節中見到了這個問題。我們在函數參數傳遞過程中仍然使用了接口指針。
view plaincopy to clipboardprint?void SomeApp( IHello * pHello )
{
CComPtr<IHello> pCopy = pHello;
OtherApp();
pCopy->Hello();
}
void SomeApp( IHello * pHello )
{
CComPtr<IHello> pCopy = pHello;
OtherApp();
pCopy->Hello();
}
你可能還會有個疑惑,為什麼SomeApp( IHello * pHello )這個函數中的參數pHello不使用智能指針呢?
將智能指針放置到函數參數中這種做法並非不行,而是出於以下考慮我們選擇了接口指針作為參數傳遞的類型:
1.如果使用了智能指針,那麼此接口將無法在IDL文件中描述。
2.使用智能指針作為參數傳遞會增加接口的復雜度,造成理解上的困難。
3.使用如果使用第三方的智能指針作為參數的類型,則發布接口的時候需要將此智能指針的實現連同接口一起發布出去。若用戶得不到此智能指針的實現,則無法使用此接口。
4.智能指針在傳遞過程中會產生更大的開銷,從而降低程序的性能。
……
若智能指針出現在函數返回值之中,則危害會更大。試想一下如下代碼,將會產生何種後果?
view plaincopy to clipboardprint?CComPtr<IView> GetView(int nIndex)
{
CComPtr<IView> spView = NULL;
spView .CreateInstance(CLSID_MYCOMPONENT);
return spView ;
}
CComPtr<IView> GetView(int nIndex)
{
CComPtr<IView> spView = NULL;
spView .CreateInstance(CLSID_MYCOMPONENT);
return spView ;
}
但外面的調用過程如果是這樣呢?
view plaincopy to clipboardprint?IView *pIView = NULL;
pIView = GetView();
pIView->DoSomething();
IView *pIView = NULL;
pIView = GetView();
pIView->DoSomething();
我們分析一下過程,GetView()返回了一個臨時對象,這個對象是個智能指針,此時他的引用計數為1。當它將此智能指針賦值給另外一個接口指針之後它便析構了。此時引用計數歸0,因此COM組件被釋放掉。之後隨著pIview調用DoSomething()程序崩潰了~
現在你應該可以肯定這一條款:混用智能指針和接口指針會使得引用計數難以琢磨。但函數參數傳遞和函數返回值中,我們允許接口指針的存在。
作者“liuchang5的專欄”