條款8:對智能指針的使用規則爛熟於心
我們在第一章的時候接觸到了普通接口引用計數的規則(條款2)。那麼在開始這一章節之前,我們來看一下使用智能指針要遵循哪些規則。智能指針的使用規則相對於接口指針會更加復雜,但考慮到他所提供的便利以及安全性。或許謹記這些規則是值得的:
1.資源申請之時將其立即捆綁到一個智能指針之上(RAII)。詳見條款1。
2.函數返回一個接口指針之前,調用Dettach()及時將智能指針與接口指針解綁定。詳見條款12。
3.智能指針需使用Attach()函數接收來自某一函數返回值中的接口指針。詳見條款12。
4.利用傳出參數傳出接口指針時,需要保證傳入的智能指針為空。詳見條款25。
條款9:盡可能不將智能指針放置於堆上
先來看一段哭笑不得的代碼:
view plaincopy to clipboardprint?void SomeApp()
{
CComPtr<ICalculator>* pspMyCalculator = new CComPtr<ICalculator>;
(*pspMyCalculator).CoCreateInstance(CLSID_CALCULATOR);
pspMyCalculator->DoSomething();
delete pspMyCalculator;
}
void SomeApp()
{
CComPtr<ICalculator>* pspMyCalculator = new CComPtr<ICalculator>;
(*pspMyCalculator).CoCreateInstance(CLSID_CALCULATOR);
pspMyCalculator->DoSomething();
delete pspMyCalculator;
}
我擔保你不會寫出像上面那樣的代碼,原因是他將“智能”這頂桂冠從智能指針頭上又摘了下來。當然,賦值運算符重載中仍將調用AddRef,智能指針也仍舊給我們提供類型安全的特性。但異常安全,自動釋放引用計數,已經離我們遠去。想想DoSomething()拋出一個異常,會發生什麼?資源洩漏了……
或許你說你永遠不會傻到寫出上面這樣的代碼來。然後,類似的悲劇還是發生了,只不過它更加隱晦:
view plaincopy to clipboardprint?class MyClass
{
public:
MyClass();
~MyClass();
DoSomething();
private:
CComPtr<ICalculator> m_spCalculator;
};
class MyClass
{
public:
MyClass();
~MyClass();
DoSomething();
private:
CComPtr<ICalculator> m_spCalculator;
};
這個聲明沒有問題,但是下面的使用方式卻帶來了一個潛在問題。
view plaincopy to clipboardprint?void SomeApp()
{
MyClass* pMyClass = new MyClass(); // 哦~ 果然還是出問題了。
...
delete pMyClass;
}
void SomeApp()
{
MyClass* pMyClass = new MyClass(); // 哦~ 果然還是出問題了。
...
delete pMyClass;
}
試想一下上面代碼會發生什麼,情況不容樂觀。智能指針出棧的條件是delete pMyClass。但如果他永遠無法執行到呢?內存洩漏又發生了。
或許你會說這種問題還是太過於明顯,但我的舉例只是讓你明白問題的所在。在正真的代碼中MyClass可能有是另外一個堆上對象的成員屬性,而這個堆上對象的申請過程可能在一個Init()函數中,釋放過程卻在UnInit()函數中,這中間相差十萬八千裡。要發現他是多麼的困難。
因此忠告是“盡可能不將智能指針放置於堆上”,對於堆上出現的智能指針一定要嚴格提防。但是你可能還有最後一個疑惑:為什麼是“盡可能”而不是“切勿”?難道某些時候我們還是需要堆上的智能指針嗎?
答案是肯定的,我們需要堆上的智能指針。你可能會問,如果我們確實需要一個在堆上的資源,那應該如何做呢?
一種特殊情況是資源的生命周期與程序進程相同,因此而不存在提前釋放資源的難題,如下這種做法。那它是安全的:
view plaincopy to clipboardprint?class TheApp
{
public:
TheApp();
~TheApp();
DoSomething();
private:
CComPtr<IDocument> m_spDocument; //這個接口在整個程序生命周期中使用
};
TheApp g_theApp; //全局對象會在堆上開辟空間。
TheApp *g_pTheApp = NULL; //或者采用這種方式,在另一個地方new出來。
class TheApp
{
public:
TheApp();
~TheApp();
DoSomething();
private:
CComPtr<IDocument> m_spDocument; //這個接口在整個程序生命周期中使用
};
TheApp g_theApp; //全局對象會在堆上開辟空間。
TheApp *g_pTheApp = NULL; //或者采用這種方式,在另一個地方new出來。
若的生命周期是全局的,那就將其放置到堆上不會出現太多問題。畢竟他可能需要在程序結束之後才釋放。但若他的生命周期並非全局,而又需要動態申請的情況呢?
正確的做法是再用一個資源管理對象或智能指針對象負責此堆上的資源。他大概會像如像這樣:
view plaincopy to clipboardprint?class TheApp //若TheApp需要最堆上申請
{
public:
TheApp();
~TheApp();
DoSomething();
private:
CComPtr<IDocument> m_spDocument; //一個會出現在堆中的智能指針
};
auto_ptr<TheApp> func()
{
auto_ptr<TheApp> spTheApp(new TheApp); //用另一個智能指針負責堆上資源。
...
return spTheApp; //將其傳遞出去,以延續堆上對象的生命周期
}
class TheApp //若TheApp需要最堆上申請
{
public:
TheApp();
~TheApp();
DoSomething();
private:
CComPtr<IDocument> m_spDocument; //一個會出現在堆中的智能指針
};
auto_ptr<TheApp> func()
{
auto_ptr<TheApp> spTheApp(new TheApp); //用另一個智能指針負責堆上資源。
...
return spTheApp; //將其傳遞出去,以延續堆上對象的生命周期
}
請記住“盡可能不將智能指針放置於堆上”。OK,結束了~
作者“liuchang5的專欄”