在《C++ 程式設計語言》等經典著作中,虛函數的實現機制有詳細闡述:編譯器透過虛函數表(VTABLE)在編譯期插入隱藏代碼,保存類型資訊與虛函數位址,執行期根據實際物件類型查找對應的函數實作。本文將示範如何在 C 語言中手動模擬這一機制,實現類似 C++ 的多態行為。
網路上常見的做法是在基類中保存派生類指標,但這違背了多態的本質——多態應透過基類介面使用派生類,基類不應依賴派生類資訊。本文採用的方案是:
void** vtable 成員作為函數指標陣列,並以 int vt_size 記錄陣列長度struct base {
void **vtable;
int vt_size;
void (*func_1)(struct base *b);
int (*func_2)(struct base *b, int x);
};
struct derived {
struct base b;
int i;
};
struct derived_2 {
struct derived d;
char *name;
};
注意:derived 和 derived_2 並未額外聲明 func_1、func_2。在 C 語言虛函數實現中,派生類覆寫虛函數時無需在結構體中重複聲明,只需在實作檔案中定義覆寫函數,並在建構函數中將其填入虛函數表。
面臨的問題:派生類不知道基類函數實作位置(高內聚、低耦合原則),建構派生類實例時如何初始化虛函數表?C++ 編譯器會自動調用父類建構函數,C 語言需手動協調。
解決方案:每個類提供兩個函數——
new_derived()):供使用者建立實例initialize_derived()):供直接繼承的子類調用,確保虛函數表正確初始化派生類只需調用直接基類的初始化函數,層層遞進,保證虛函數表正確構建。
struct derived *new_derived() {
struct derived *d = malloc(sizeof(struct derived));
initialize_base((struct base*)d);
initialize_derived(d); // 設置或修改 VTABLE
return d;
}
刪除物件時需從派生類析構函數依次調用至基類。在基類中顯式聲明析構函數指標 deletor,基類實作中反向遍歷虛函數表調用:
struct base {
void **vtable;
int vt_size;
void (*func_1)(struct base *b);
int (*func_2)(struct base *b, int x);
void (*deletor)(struct base *b);
};

假設 derived 實作三個虛函數,derived_2 實作兩個(func_2 未覆寫),derived_2 實例的最終虛函數表長度(vt_size)為 9;derived 實例則為 6。關鍵限制:基類必須實作所有虛函數,否則機制無法運轉——所有調用從基類實作進入,透過遍歷虛函數表尋找派生類實作。
此機制下,透過基類指標存取派生類實例時,行為基本與 C++ 一致。初始化函數如何裝配虛函數表、基類虛函數實作細節,可依上述思路自行編寫。這種手動實現雖較繁瑣,但深入理解後有助於掌握物件導向核心機制的本質。