在《 C++ 編程思想》一書中對(duì)虛函數(shù)的實(shí)現(xiàn)機(jī)制有詳細(xì)的描述,一般的編譯器通過(guò)虛函數(shù)表,在編譯時(shí)插入一段隱藏的代碼,保存類型信息和虛函數(shù)地址,而在調(diào)用時(shí),這段隱藏的代碼可以找到和實(shí)際對(duì)象一致的虛函數(shù)實(shí)現(xiàn)。
我們?cè)谶@里提供一個(gè) C 中的實(shí)現(xiàn),模仿 VTABLE 這種機(jī)制,但一切都需要我們自己在代碼中裝配。
之前在網(wǎng)上看到一篇描述 C 語(yǔ)言實(shí)現(xiàn)虛函數(shù)和多態(tài)的文章,談到在基類中保存派生類的指針、在派生類中保存基類的指針來(lái)實(shí)現(xiàn)相互調(diào)用,保障基類、派生類在使用虛函數(shù)時(shí)的行為和 C++ 類似。我覺(jué)得這種方法有很大的局限性,不說(shuō)繼承層次的問(wèn)題,單單是在基類中保存派生類指針這一做法,就已經(jīng)違反了虛函數(shù)和多態(tài)的本意——多態(tài)就是要通過(guò)基類接口來(lái)使用派生類,如果基類還需要知道派生類的信息……。
我的基本思路是:
- 在“基類”中顯式聲明一個(gè) void** 成員,作為數(shù)組保存基類定義的所有函數(shù)指針,同時(shí)聲明一個(gè) int 類型的成員,指明 void* 數(shù)組的長(zhǎng)度。
- “基類”定義的每個(gè)函數(shù)指針在數(shù)組中的位置、順序是固定的,這是約定,必須的
- 每個(gè)“派生類”都必須填充基類的函數(shù)指針數(shù)組(可能要?jiǎng)討B(tài)增長(zhǎng)),沒(méi)有重寫虛函數(shù)時(shí),對(duì)應(yīng)位置置 0
- “基類”的函數(shù)實(shí)現(xiàn)中,遍歷函數(shù)指針數(shù)組,找到繼承層次中的最后一個(gè)非 0 的函數(shù)指針,就是實(shí)際應(yīng)該調(diào)用的和對(duì)象相對(duì)應(yīng)的函數(shù)實(shí)現(xiàn)
好了,先來(lái)看一點(diǎn)代碼:
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; };上面的代碼是我們接下來(lái)要討論的,先說(shuō)一點(diǎn),在 C 中,用結(jié)構(gòu)體內(nèi)的函數(shù)指針和 C++ 的成員函數(shù)對(duì)應(yīng), C 的這種方式,所有函數(shù)都天生是虛函數(shù)(指針可以隨時(shí)修改哦)。
注意,derived 和 derived_2 并沒(méi)有定義 func_1 和 func_2 。在 C 的虛函數(shù)實(shí)現(xiàn)中,如果派生類要重寫虛函數(shù),不需要在派生類中顯式聲明。要做的是,在實(shí)現(xiàn)文件中實(shí)現(xiàn)你要重寫的函數(shù),在構(gòu)造函數(shù)中把重寫的函數(shù)填入虛函數(shù)表。
我們面臨一個(gè)問(wèn)題,派生類不知道基類的函數(shù)實(shí)現(xiàn)在什么地方(從高內(nèi)聚、低耦合的原則來(lái)看),在構(gòu)造派生類實(shí)例時(shí),如何初始化虛函數(shù)表?在 C++ 中編譯器會(huì)自動(dòng)調(diào)用繼承層次上所有父(祖先)類的構(gòu)造函數(shù),也可以顯式在派生類的構(gòu)造函數(shù)的初始化列表中調(diào)用基類的構(gòu)造函數(shù)。怎么辦?
我們提供一個(gè)不那么優(yōu)雅的解決辦法:
每個(gè)類在實(shí)現(xiàn)時(shí),都提供兩個(gè)函數(shù),一個(gè)構(gòu)造函數(shù),一個(gè)初始化函數(shù),前者用戶生成一個(gè)類,后者用于繼承層次緊接自己的類來(lái)調(diào)用以便正確初始化虛函數(shù)表。依據(jù)這樣的原則,一個(gè)派生類,只需要調(diào)用直接基類的初始化函數(shù)即可,每個(gè)派生類都保證這一點(diǎn),一切都可以進(jìn)行下去。
下面是要實(shí)現(xiàn)的兩個(gè)函數(shù):
struct derived *new_derived(); void initialize_derived(struct derived *d);new 開(kāi)頭的函數(shù)作為構(gòu)造函數(shù), initialize 開(kāi)頭的函數(shù)作為 初始化函數(shù)。我們看一下 new_derived 這個(gè)構(gòu)造函數(shù)的實(shí)現(xiàn)框架:
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 的構(gòu)造函數(shù) new_derived_2,那么只需要調(diào)用 initialize_derived 即可。
說(shuō)完了構(gòu)造函數(shù),對(duì)應(yīng)的要說(shuō)析構(gòu)函數(shù),而且析構(gòu)函數(shù)要是虛函數(shù)。在刪除一個(gè)對(duì)象時(shí),需要從派生類的析構(gòu)函數(shù)依次調(diào)用到繼承層次最頂層的基類的析構(gòu)函數(shù)。這點(diǎn)在 C 中也是可以保障的。做法是:給基類顯式聲明一個(gè)析構(gòu)函數(shù),基類的實(shí)現(xiàn)中查找虛函數(shù)表,從后往前調(diào)用即可。函數(shù)聲明如下:
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); };
說(shuō)完構(gòu)造、析構(gòu),該說(shuō)這里的虛函數(shù)表到底是怎么回事了。我們先畫個(gè)圖,還是以剛才的 base 、 derived 、derived_2 為例來(lái)說(shuō)明,一看圖就明白了:
我們假定 derived 類實(shí)現(xiàn)了三個(gè)虛函數(shù), derived_2 類實(shí)現(xiàn)了兩個(gè),func_2 沒(méi)有實(shí)現(xiàn),上圖就是 derived_2 的實(shí)例所擁有的最終的虛函數(shù)表,表的長(zhǎng)度( vt_size )是 9。如果是 derived 的實(shí)例,就沒(méi)有表中的最后三項(xiàng),表的長(zhǎng)度( vt_size )是 6 。
必須限制的是:基類必須實(shí)現(xiàn)所有的虛函數(shù),只有這樣,這套實(shí)現(xiàn)機(jī)制才可以運(yùn)轉(zhuǎn)下去。因?yàn)橐磺械陌l(fā)生是從基類的實(shí)現(xiàn)函數(shù)進(jìn)入,通過(guò)遍歷虛函數(shù)表來(lái)找到派生類的實(shí)現(xiàn)函數(shù)的。
當(dāng)我們通過(guò) base 類型的指針(實(shí)際指向 derived_2 的實(shí)例)來(lái)訪問(wèn) func_1 時(shí),基類實(shí)現(xiàn)的 func_1 會(huì)找到 VTABLE 中的 derived_2_func_1 進(jìn)行調(diào)用。
好啦,到現(xiàn)在為止,基本說(shuō)明白了實(shí)現(xiàn)原理,至于 初始化函數(shù)如何裝配虛函數(shù)表、基類的虛函數(shù)實(shí)現(xiàn),可以根據(jù)上面的思路寫出代碼來(lái)。按照我的這種方法實(shí)現(xiàn)的虛函數(shù),通過(guò)基類指針訪問(wèn),行為基本和 C++ 一致。
回顧一下:
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
