有效的使用和設計COM智能指針——條款1:智能指針之前世今生

條款1:智能指針之前世今生

更多條款請前往原文出處:http://blog.csdn.net/liuchang5

我最初研究COM引用計數和智能指針時候,是先從編寫_com_ptr_tCComPtrAPI文檔開始的。那時,我的項目經理曾多次問我_com_ptr_tCComPtr的一些的背景。諸如:它所屬的庫,產生的歷史原因。並提議我將這些內容融入到文檔中去。

這是個很好的建議,我這麼做了。取得的效果也覺得十分只令人滿意。最明顯之處在於,原本需要大篇幅解釋的問題會因爲前面一個背景簡介,而縮小很多篇幅。讀者也便於理解。當然如果我堅持這麼繼續做下去的話可能會帶來更多的附加好處:乾巴巴的“紀傳體”API文檔宛若變成了“編年體”小說。確實能夠讓每天趴在電腦前,文檔看到昏昏欲睡的程序員提起不少精神。

因此也不妨讓我們瞭解更多關於COM接口的智能指針的其他內容前,回顧一下智能指針產生的歷史原因。這可能會幫你回答更多的“爲什麼?”。

在只有C的時代,對於資源釋放的問題,向來是留給程序員手動完成的。如:有molloc應當存在與之對應的free。使用fopen打開文件就應當有fclose將文件關閉。這些程序員來完成,都是天經地義的。若是使用完資源而沒有將其返還給系統,那麼我們更多的情況下認爲這個程序存在着某種資源泄漏,需要對其進行修改。

貌似一個mallocfree成對出現並不難,只要一個程序員不疏忽而認真編碼這類問題都可以解決。資源釋放與回收只是個態度問題,而並非一個技術性難題。當時很多公司要求函數單入單出,以保證所有資源在函數出口處被釋放。

但是隨着語言的進一步發展,一些新的C++的語言特性引入到我們的程序之中,這使得我們難以保證資源被釋放的過程必定發生。最明顯的便是異常:

void f()
{
    Investment* pInv = new Investment(123);
    ...
    delete pInv;
}


經過前面的介紹,應當會發現“...”可能纔在一個return語句,不小心跳過了後面的delete。這或許只是某個程序員的態度問題。也或許是在其他程序員修改編碼的時候,疏忽所造成的。但如果這個地方拋出了一個異常呢?程序軌跡被打亂了,你可能不得不下大精力來寫一些煩人的try{}catch{}語句,然後針對不同情況做出處理並釋放相應的資源。若可能拋出異常的情況很多呢?額~稍微有經驗的程序員用大腿想想都能知道try{}catch{}層層嵌套帶來的邏輯有多麼複雜!

於是乎資源釋放從態度問題升級爲一個技術性難題。怎麼有效管理資源?以不至於泄漏?

於是最初想到的辦法是通過一個棧上(stack-based)的資源管理一個堆上(heap-based)資源。如果利用棧上對象出棧過程所調用的析構函數來釋放掉所持有的堆上資源,那無論何種情況下資源都不會泄漏了。這確實是個絕妙的注意,也奠定了智能指針的一個核心思想:“用棧中對象管理堆上或外部資源”這個在棧上創建的對象名曰“資源管理對象”,確實很形象。

void f()
{
    Investment* pInv = new Investment(123);
    InvestmentManager investmentManager(pInv); //將資源交給一個資源管理對象使用
    //不管這後面出現什麼問題,只要函數出棧了,資源都能被自動釋放
    investmentManager.getInvestment().DoSomeThing();
    ...  
}


之後種種思想進一步升級成了C++中的一個慣用法(idiom)名曰“RAIIResource Acquisition Is Initialization”貌似理解起來有點困難。Bjarne Stroustrup在的《C++程序設計語言(第3版)》【14】一書中是這樣解釋的:使用局部對象管理資源的技術通常稱爲“資源獲取就是初始化RAII”。這種通用技術依賴於構造函數和析構函數的性質以及它們與異常處理的交互作用。

這又與之前的資源管理思想有些什麼不同之處呢?RAII更加強調的是將資源獲取與一個對象的初始化過程緊密的綁定在一起,在看RAII做法之前我們看一下如果沒有這種強調,程序會出現什麼問題。

void f()
{
    Investment* pInv = new Investment(123);
    Profit* pPro = new Profit(123);  //關注這裏,如果拋出異常會怎樣?
    InvestmentManager investmentManager(pInv);  
    ProfitManager profitManager(pPro); 
}


雖然有了資源管理對象,但是如果在資源申請後沒有立即將其與對象綁定起來。那資源泄漏還是發生了。

因此RAII的做法如下:


void f()
{
    InvestmentManager investmentManager(new Investment(123););  
    ProfitManager profitManager(new Profit(123); //再怎麼出錯也不會產生問題了。
    investmentManager.getInvestment().DoSomeThing();
    ... 
}


觀察上面的代碼,你會發現我們仍然有使這個問題處理得更好的空間。在最原始的程序中,外部資源的訪問都是通過指針來實現的,如文件指針、指向內存數組的指針。那麼我們是否可以將資源管理類抽象成一個類似指針的類呢?在C++爲我們提供的重載那些運算符能實現這一功能。具體討論細節可以參見條款19“巧妙的將對象僞裝成指針”於是他之後呈現出的形式更加的簡潔,更加類似於我們慣用的普通指針。這樣做的好處是異常情況下資源不會泄漏,但值得注意的是他確實沒有將資源管理的問題從程序員的編碼問題中解決掉。如果你對此疑惑不解,請回顧一下條款5所講述的內容“必須手動釋放COM組件式,別妄想智能指針幫你完成”。用完後將資源及時返回給系統,仍然是程序員需要自己解決的問題。

void f()
{
    InvestmentPtr pInvestment = new Investment(123);//重載構造函數實現RIIA
    ProfitManagerPtr pProfit = new Profit(123); 
    pInvestment ->DoSomeThing();//這個動作太酷了,它儼然就是一個指針!
    ... 
}


OK智能指針誕生了或許可以給他套上template這頂小帽子,他將呈現出更加的通用的形式。

void f()
{
    MySmartPtr<Investment> pInvestment = new Investment(123);
    MySmartPtr<Profit> pProfit  = new Profit(123); //智能指針可以複用了
    pInvestment ->DoSomeThing();//這個動作太酷了,它儼然就是一個指針!
    ... 
}


你可能會詢問智能指針的更多細節,但讓我們就此打住一下。讓我們先看看引用計數以及如何用智能指針簡化COM開發的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章