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

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫(xiě)作最大的動(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ì)您有幫助就好】元
