初次研究 COM 引用計數與智能指標時,我從 _com_ptr_t 與 CComPtr 的 API 文件入手。專案經理多次詢問它們的所屬庫與歷史緣起,並建議將背景融入文件。這確實是個絕佳建議——原本需大段解釋的問題,因前情提要而大幅精簡;枯燥的「紀傳體」API 文檔搖身一變成「編年體」小說,讓天天趴在電腦前看文件昏昏欲睡的程式設計師也能提振精神。
因此,在深入技術細節前,讓我們先回顧智能指標誕生的歷史脈絡,這能幫你回答更多「為什麼」。
在唯有 C 的年代,資源釋放全靠程式設計師手動完成:malloc 配 free,fopen 配 fclose。只要程式設計師不疏忽、認真編碼,成對出現並不難。當時許多公司規定函數「單入單出」,確保所有資源在函數出口釋放——資源回收只是態度問題,而非技術難題。
隨著 C++ 語言特性(特別是異常)引入,我們難以保證資源釋放必定發生:
void f() {
Investment* pInv = new Investment(123);
... // 若此處拋出異常或提前 return
delete pInv; // 永遠不會執行 → 記憶體洩漏
}
若在「...」處拋出異常,程式流程被打亂,不得不寫層層巢狀的 try{}catch{} 來釋放資源——邏輯複雜度指數級上升。資源釋放從「態度問題」升級為「技術性難題」:如何有效管理資源,以不至於洩漏?
最初的解法是利用棧上物件管理堆上資源:棧上物件出作用域時自動呼叫解構函數釋放堆上資源,無論何種情況資源都不會洩漏。這奠定了智能指標的核心思想——「用棧中物件管理堆上或外部資源」,此棧上物件稱為「資源管理物件」。
void f() {
Investment* pInv = new Investment(123);
InvestmentManager investmentManager(pInv); // 交給資源管理物件
// 任何情況下函數出棧,資源皆自動釋放
investmentManager.getInvestment().DoSomething();
...
}
上述思想進一步昇華為 C++ 經典慣用法——RAII (Resource Acquisition Is Initialization)。史特勞斯特魯普在《C++ 程式設計語言》中解釋:使用區域物件管理資源的技術稱為 RAII,依賴建構函數、解構函數特性及其與異常處理的交互作用。
RAII 強調將資源獲取與物件初始化緊密綁定。若未綁定,資源洩漏仍會發生:
// 錯誤示範:資源申請與綁定分離
Investment* pInv = new Investment(123);
Profit* pPro = new Profit(123); // 若此處拋出異常,pInv 洩漏
InvestmentManager investmentManager(pInv);
ProfitManager profitManager(pPro);
// RAII 正確做法:申請即綁定
InvestmentManager investmentManager(new Investment(123));
ProfitManager profitManager(new Profit(123)); // 再怎麼出錯也不會洩漏
觀察上述程式碼,仍有改進空間。早期透過指標存取外部資源(檔案指標、記憶體陣列指標),能否將資源管理類抽象成類指標物件?C++ 提供的運算子重載(operator->, operator*)讓這成為可能——詳見條款 19「巧妙將物件偽裝成指標」。
void f() {
InvestmentPtr pInvestment = new Investment(123); // 重載建構函數實現 RAII
ProfitManagerPtr pProfit = new Profit(123);
pInvestment->DoSomething(); // 簡潔如指標操作
...
}
再套上 template,智能指標呈現通用形態:
void f() {
MySmartPtr<Investment> pInvestment = new Investment(123);
MySmartPtr<Profit> pProfit = new Profit(123); // 智能指標可復用
pInvestment->DoSomething();
...
}
智能指標由此誕生。但注意:它並未將資源管理問題從程式設計師編碼責任中完全解脫——用完後及時歸還資源,仍是程式設計師的責任(參見條款 5)。
暫且打住,讓我們轉向引用計數與智能指標如何簡化 COM 開發的核心議題。
—— 作者:liuchang5 的專欄