面向物件技術源於 1960 年代的 Simula 67,在 Smalltalk 中成熟。對多數程式設計師而言,C++ 是首個可用的 OOP 語言,我們的 OOP 觀念多源於 C++。然而,C++ 實現關鍵的多態性時,選擇了與 Smalltalk 截然不同的方案。表面相似,實踐卻天差地別:C++ 多態極致高效,卻不適用所有場合。經驗不足者強行套用,陷入「削足適履」泥沼。本文深度探討 C++ 多態的局限與破解之道。
多態核心:透過基類指標調用虛函數,運行時根據實際物件動態分派。編譯器需增加一個「間接層」攔截調用,完成三步:獲取方法資訊、獲取實際物件、查表分派。關鍵在於第 3 步如何查表。
C++ 選擇方案 B 符合其「零開銷抽象」哲學,但將適用場景鎖定在「虛函數少、覆寫率高」的稠密矩陣型場景。
類比數值分析中的稠密矩陣與稀疏矩陣,多態實踐中存在兩種性質迥異的場景:
Modem 類:虛函數難超過 20 個,不同實現常覆寫大部分甚至全部。IMalloc 6 方法,IPersistFile 5 方法,實現幾乎全覆寫。DefWindowProc 統一處理或轉發,僅攔截幾個至幾十個,覆寫率通常 ≤ 20%。KWindow 定義 1000 虛函數 → 所有派生類強制擁有 1000 項虛表。KPushButton 僅覆寫 20 個,虛表 980 項浪費。更極端:KMyPushButtonOK 僅覆寫 1 個 OnClick,虛表 999 項浪費(32 位機佔 3996 位元組)。百個自訂控件 → 虛表浪費數百 KB 甚至 1 MB+。這正是 OWL、VCL、MFC、Qt 等所有成熟 C++ GUI 框架無一使用標準 C++ 多態構造視窗類層次的根本原因。它們各自發明機制繞過暗礁:VCL 動態方法、MFC 全域事件查找表、Qt Signal/Slot。核心思想一致——Grady Booch 所言:「當系統需要大量相似小型類時,應用大量相似小型物件解決。」將「派生新類」轉為「實例化新物件」,不可避免需要類似 C# delegate 的機制。標準 C++ 不支援 delegate,Borland 擴展
__closure,MFC 發明巨型巨集,Qt 搞moc前處理器,八仙過海。
C++ 選擇的多態實現符合其哲學,且允許開發者以各種方式解決問題。成熟 GUI 框架已自動繞過暗礁。嚴重性在於教育缺失:許多開發者不了解原生多態在場景二中的局限,面對類似問題不自覺踏入陷阱。
當系統含標準事件處理特徵且事件數量較大時,請慎重設計類層次。可參考 MFC/Qt 方案,或採用更直接簡單的方法:模擬按名稱查找的方法表,用單一訊息分發物件向各物件分發訊息。將分發物件獨立放入 DLL 或 COM 組件,運行時載入。此方案非最精巧,但多數情況有效且實現簡單。
編譯器可在編譯期獲得「基類虛函數多、派生類覆寫少」資訊,自動切換派生類查找機制:從按位置改為按實際函數資訊查找。派生類虛表無需與基類保持結構一致,避免空間浪費。此思路類似 Delphi/Object Pascal 的 dynamic 關鍵字。
面向物件多態有兩種應用場景,C++ 標準多態僅適配其一。識別場景、選擇正確機制,是 C++ 進階開發者的必修課。不要讓「高效」的虛函數表成為架構的腳鐐。