在《 C++ 編程思想》一書中對虛函數的實現機制有詳細的描述,一般的編譯器通過虛函數表,在編譯時間插入一段隱藏的代碼,檔案類型資訊和虛函數地址,而在調用時,這段隱藏的代碼可以找到和實際對象一致的虛函數實現。
我們在這裡提供一個 C 中的實現,模仿 VTABLE 這種機制,但一切都需要我們自己在代碼中裝配。
之前在網上看到一篇描述 C 語言實現虛函數和多態的文章,談到在基類中儲存衍生類別的指標、在衍生類別中儲存基類的指標來實現相互調用,保障基類、衍生類別在使用虛函數時的行為和 C++ 類似。我覺得這種方法有很大的局限性,不說繼承層次的問題,單單是在基類中儲存衍生類別指標這一做法,就已經違反了虛函數和多態的本意——多態就是要通過基類介面來使用衍生類別,如果基類還需要知道衍生類別的資訊……。
我的基本思路是:
在“基類”中顯式聲明一個 void** 成員,作為數組儲存基類定義的所有函數指標,同時聲明一個 int 類型的成員,指明 void* 數組的長度。 “基類”定義的每個函數指標在數組中的位置、順序是固定的,這是約定,必須的 每個“衍生類別”都必須填充基類的函數指標數組(可能要動態增長),沒有重寫虛函數時,對應位置置 0 “基類”的函數實現中,遍曆函數指標數組,找到繼承層次中的最後一個非 0 的函數指標,就是實際應該調用的和對象相對應的函數實現
好了,先來看一點代碼:
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;}; 上面的代碼是我們接下來要討論的,先說一點,在 C 中,用結構體內的函數指標和 C++ 的成員函數對應, C 的這種方式,所有函數都天生是虛函數(指標可以隨時修改哦)。
注意,derived 和 derived_2 並沒有定義 func_1 和 func_2 。在 C 的虛函數實現中,如果衍生類別要重寫虛函數,不需要在衍生類別中顯式聲明。要做的是,在實現檔案中實現你要重寫的函數,在建構函式中把重寫的函數填入虛函數表。
我們面臨一個問題,衍生類別不知道基類的函數實現在什麼地方(從高內聚、低耦合的原則來看),在構造衍生類別執行個體時,如何初始化虛函數表。在 C++ 中編譯器會自動調用繼承層次上所有父(祖先)類的建構函式,也可以顯式在衍生類別的建構函式的初始化列表中調用基類的建構函式。怎麼辦。
我們提供一個不那麼優雅的解決辦法:
每個類在實現時,都提供兩個函數,一個建構函式,一個初始化函數,前者使用者產生一個類,後者用於繼承層次緊接自己的類來調用以便正確初始化虛函數表。依據這樣的原則,一個衍生類別,只需要調用直接基類的初始化函數即可,每個衍生類別都保證這一點,一切都可以進行下去。
下面是要實現的兩個函數:
struct derived *new_derived();void initialize_derived(struct derived *d);
new 開頭的函數作為建構函式, initialize 開頭的函數作為 初始化函數。我們看一下 new_derived 這個建構函式的實現架構:
struct derived *new_derived(){ struct derived * d = malloc(sizeof(struct derived)); initialize_base((struct base*)d); initialize_derived(d);/* setup or modify VTABLE */ return d;} 如果是 derived_2 的建構函式 new_derived_2,那麼只需要調用 initialize_derived 即可。
說完了建構函式,對應的要說解構函式,而且解構函式要是虛函數。在刪除一個對象時,需要從衍生類別的解構函式依次調用到繼承層次最頂層的基類的解構函式。這點在 C 中也是可以保障的。做法是:給基類顯式聲明一個解構函式,基類的實現中尋找虛函數表,從後往前調用即可。函式宣告如下:
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);};
說完構造、析構,該說這裡的虛函數表到底是怎麼回事了。我們先畫個圖,還是以剛才的 base 、 derived 、derived_2 為例來說明,一看圖就明白了:
我們假定 derived 類實現了三個虛函數, derived_2 類實現了兩個,func_2 沒有實現,上圖就是 derived_2 的執行個體所擁有的最終的虛函數表,表的長度( vt_size )是 9 。如果是 derived 的執行個體,就沒有表中的最後三項,表的長度( vt_size )是 6 。
必須限制的是:基類必須實現所有的虛函數,只有這樣,這套實現機制才可以運轉下去。因為一切的發生是從基類的實現函數進入,通過遍曆虛函數表來找到衍生類別的實現函數的。
當我們通過 base 類型的指標(實際指向 derived_2 的執行個體)來訪問 func_1 時,基類實現的 func_1 會找到 VTABLE 中的 derived_2_func_1 進行調用。
好啦,到現在為止,基本說明白了實現原理,至於 初始化函數如何裝配虛函數表、基類的虛函數實現,可以根據上面的思路寫出代碼來。按照我的這種方法實現的虛函數,通過基類指標訪問,行為基本和 C++ 一致。
回顧一下:
C語言物件導向編程(一):封裝與繼承 C語言物件導向編程(二):繼承詳解