C語言面向對象編程(三):虛函數與多態

 在《 C++ 編程思想》一書中對虛函數的實現機制有詳細的描述,通常的編譯器經過虛函數表,在編譯時插入一段隱藏的代碼,保存類型信息和虛函數地址,而在調用時,這段隱藏的代碼能夠找到和實際對象一致的虛函數實現。編程

    咱們在這裏提供一個 C 中的實現,模仿 VTABLE 這種機制,但一切都須要咱們本身在代碼中裝配。數組

    以前在網上看到一篇描述 C 語言實現虛函數和多態的文章,談到在基類中保存派生類的指針、在派生類中保存基類的指針來實現相互調用,保障基類、派生類在使用虛函數時的行爲和 C++ 相似。我以爲這種方法有很大的侷限性,不說繼承層次的問題,單單是在基類中保存派生類指針這一作法,就已經違反了虛函數和多態的本意——多態就是要經過基類接口來使用派生類,若是基類還須要知道派生類的信息……。框架

    個人基本思路是:函數

  • 在「基類」中顯式聲明一個 void** 成員,做爲數組保存基類定義的全部函數指針,同時聲明一個 int 類型的成員,指明 void* 數組的長度。spa

  • 「基類」定義的每一個函數指針在數組中的位置、順序是固定的,這是約定,必須的.net

  • 每一個「派生類」都必須填充基類的函數指針數組(可能要動態增加),沒有重寫虛函數時,對應位置置 0指針

  • 「基類」的函數實現中,遍歷函數指針數組,找到繼承層次中的最後一個非 0 的函數指針,就是實際應該調用的和對象相對應的函數實現orm

    好了,先來看一點代碼:對象

[cpp] view plain copyblog

  1. struct base {  

  2.     void ** vtable;  

  3.     int vt_size;  

  4.       

  5.     void (*func_1)(struct base *b);  

  6.     int (*func_2)(struct base *b, int x);  

  7. };  

  8.   

  9. struct derived {  

  10.     struct base b;  

  11.     int i;  

  12. };  

  13.   

  14. struct derived_2{  

  15.     struct derived d;  

  16.     char *name;  

  17. };  

    上面的代碼是咱們接下來要討論的,先說一點,在 C 中,用結構體內的函數指針和 C++ 的成員函數對應, C 的這種方式,全部函數都天生是虛函數(指針能夠隨時修改哦)。

    注意,derived 和 derived_2 並無定義 func_1 和 func_2 。在 C 的虛函數實現中,若是派生類要重寫虛函數,不須要在派生類中顯式聲明。要作的是,在實現文件中實現你要重寫的函數,在構造函數中把重寫的函數填入虛函數表。

    咱們面臨一個問題,派生類不知道基類的函數實如今什麼地方(從高內聚、低耦合的原則來看),在構造派生類實例時,如何初始化虛函數表?在 C++ 中編譯器會自動調用繼承層次上全部父(祖先)類的構造函數,也能夠顯式在派生類的構造函數的初始化列表中調用基類的構造函數。怎麼辦?

    咱們提供一個不那麼優雅的解決辦法:

    每一個類在實現時,都提供兩個函數,一個構造函數,一個初始化函數,前者用戶生成一個類,後者用於繼承層次緊接本身的類來調用以便正確初始化虛函數表。依據這樣的原則,一個派生類,只須要調用直接基類的初始化函數便可,每一個派生類都保證這一點,一切均可以進行下去。

    下面是要實現的兩個函數:

[cpp] view plain copy

  1. struct derived *new_derived();  

  2. void initialize_derived(struct derived *d);  

    new 開頭的函數做爲構造函數, initialize 開頭的函數做爲 初始化函數。咱們看一下 new_derived 這個構造函數的實現框架:

[cpp] view plain copy

  1. struct derived *new_derived()  

  2. {  

  3.     struct derived * d = malloc(sizeof(struct derived));  

  4.     initialize_base((struct base*)d);  

  5.     initialize_derived(d);/* setup or modify VTABLE */  

  6.     return d;  

  7. }  

    若是是 derived_2 的構造函數 new_derived_2,那麼只須要調用 initialize_derived 便可。

    說完了構造函數,對應的要說析構函數,並且析構函數要是虛函數。在刪除一個對象時,須要從派生類的析構函數依次調用到繼承層次最頂層的基類的析構函數。這點在 C 中也是能夠保障的。作法是:給基類顯式聲明一個析構函數,基類的實現中查找虛函數表,從後往前調用便可。函數聲明以下:

[cpp] view plain copy

  1. struct base {  

  2.     void ** vtable;  

  3.     int vt_size;  

  4.       

  5.     void (*func_1)(struct base *b);  

  6.     int (*func_2)(struct base *b, int x);  

  7.     void (*deletor)(struct base *b);  

  8. };  


    說完構造、析構,該說這裏的虛函數表究竟是怎麼回事了。咱們先畫個圖,仍是以剛纔的 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++ 一致。

   回顧一下:


網友評論:

initialize 開頭的初始化函數是如何實現的?

就是給vtable分配空間,給函數指針賦值。主要幹這兩件事情。vtable是動態的,根據繼承層次多少能夠動態增加。

相關文章
相關標籤/搜索