繼承 (Inheritance)允許一個(gè)類(lèi)繼承另一個(gè)類(lèi)的數(shù)據(jù)和成員函數(shù)。例如, 考慮圖 7.19中的代碼。它展示了兩個(gè)類(lèi),A和B,其中類(lèi)B是通過(guò)繼承類(lèi)A得到的。程序的輸出如下:
Size of a: 4 Offset of ad: 0
Size of b: 8 Offset of ad: 0 Offset of bd: 4 A::m()
A::m()
注意,兩個(gè)類(lèi)的數(shù)據(jù)成員ad(B通過(guò)繼承A得到的)在相同的偏移處。這是非常重要的,因?yàn)閒函數(shù)將傳遞一個(gè)指針到一個(gè)A對(duì)象或任意一個(gè)由A派生( 也就是 ,通過(guò)繼承得到)的對(duì)象類(lèi)型中。圖 7.20展示了此函數(shù)的(編輯過(guò)的)匯編代碼(gcc得到的)。
注意在輸出中,a和b對(duì)象調(diào)用的都是A的成員函數(shù)m。從匯編程序中,我們可以看到對(duì)A::m()的調(diào)用被硬編碼到函數(shù)中了。對(duì)于真正的面向?qū)ο缶幊蹋蓡T函數(shù)的調(diào)用取決于傳遞給函數(shù)的對(duì)象類(lèi)型是什么。這就是所謂的 多態(tài) 。缺省情況下,C++關(guān)掉了這個(gè)特性。你可以使用virtual 關(guān)鍵字來(lái)激活它。圖 7.21展示了如何修改這兩個(gè)類(lèi)。其它代碼不需要修改。多態(tài)可以用許多方法來(lái)實(shí)現(xiàn)。不幸的是,當(dāng)在以這種方法書(shū)寫(xiě)的時(shí)候,gcc的實(shí)現(xiàn)方法正處在改變中,而且與它最初的實(shí)現(xiàn)方法相比,明顯變得更復(fù)雜了。為了簡(jiǎn)單化討論的目的,作者只涉及基于Microsoft和Borland編譯 器Windows使用的多態(tài)的實(shí)現(xiàn)方法。這種實(shí)現(xiàn)方法很多年沒(méi)有改變了,而且可能在未來(lái)幾年也不會(huì)改變。
有了這些改變,程序的輸出如下:
Size of a: 8 Offset of ad: 4
Size of b: 12 Offset of ad: 4 Offset of bd: 8 A::m()
B::m()
現(xiàn)在,對(duì)f的第二次調(diào)用調(diào)用了B::m()的成員函數(shù),因?yàn)樗鼈鬟f了對(duì)象B。但是,這并不是唯一的修改的地方。A的大小現(xiàn)在為8(而B(niǎo)為12)。同樣,ad的偏移為4,不是0。在偏移0處是的什么呢?這個(gè)問(wèn)題的答案與如何實(shí)現(xiàn)多態(tài)相關(guān)。
含有任意虛成員函數(shù)的C++類(lèi)有一個(gè)額外的隱藏的域,它是一張指向成員函數(shù)指針數(shù)組的指針表。這個(gè)表通常稱(chēng)為vtable。對(duì)于A和B類(lèi),指針表儲(chǔ)存在偏移地址0處。Windows編譯器總是把此指針表放到繼承樹(shù)頂部 的類(lèi)的開(kāi)始處。從擁有虛成員函數(shù)的程序版本(源自圖 7.19)中的f函數(shù)產(chǎn) 生的匯編代碼(圖 7.22)中,你可以看到對(duì)成員函數(shù)m的調(diào)用不是使用一個(gè)標(biāo)號(hào)。第9行來(lái)查找對(duì)象的vtable的地址。對(duì)象的地址在第11行中被壓入堆 棧。第12行通過(guò)分支到vtable里的第一個(gè)地址處來(lái)調(diào)用虛成員函數(shù)。這 次調(diào)用并不使用一個(gè)標(biāo)號(hào),它分支到EDX指向的代碼地址處。這種類(lèi)型的調(diào)用是一個(gè)晚綁定 (late binding)的例子。晚綁定將調(diào)用哪個(gè)成員函數(shù)的判定 延遲到代碼運(yùn)行時(shí)。這就允許代碼為對(duì)象調(diào)用恰當(dāng)?shù)某蓡T函數(shù)。標(biāo)準(zhǔn)的案 例(圖 7.20)硬編碼某個(gè)成員函數(shù)的調(diào)用,也稱(chēng)為 早綁定 (early binding) (因 為這兒成員函數(shù)被早綁定了,在編譯的時(shí)候。)。
用心的讀者將會(huì)覺(jué)得奇怪為什么在圖 7.21中的類(lèi)的成員函數(shù)通過(guò)使 用_ _cdecl關(guān)鍵字來(lái)明確聲明使用的是C調(diào)用約定。缺省情況下,Microsoft對(duì) 于C++類(lèi)成員函數(shù)使用的是不同的調(diào)用約定,而不是標(biāo)準(zhǔn)C調(diào)用約定。此調(diào)用約定將指向成員函數(shù)能起作用的對(duì)象的指針傳遞到ECX寄存器,而不 是使用堆棧。成員函數(shù)的其它明確的參數(shù)仍然使用堆棧。修改為_(kāi) _cdecl告訴編譯器使用標(biāo)準(zhǔn)C調(diào)用約定。Borland C++缺省情況下使用的是C調(diào)用約定。
下面我們?cè)倏匆粋€(gè)稍微復(fù)雜一點(diǎn)的例子。(圖 7.23)。在這個(gè)例子中, 類(lèi)A和B都有兩個(gè)成員函數(shù):m1和m2。記住因?yàn)轭?lèi)B并沒(méi)有定義自己的成員函數(shù)m2,它繼承了A類(lèi)的成員函數(shù)。圖 7.24展示了對(duì)象b在內(nèi)存中如何儲(chǔ)存。圖 7.25展示了此程序的輸出。首先,看看每個(gè)對(duì)象的vtable的地址。兩個(gè)B對(duì)象的vtable地址是一樣的,因此他們共享同樣的vtable。一 張vtable表是類(lèi)的屬性而不是一個(gè)對(duì)象(就如一個(gè)static數(shù)據(jù)成員)。其次, 看看在vtable里的地址。從匯編程序的輸出中,你可以確定成員函數(shù)m1指針在偏移地址 0處(或雙字 0)而m2在偏移地址 4處(雙字 1)。m2成員函數(shù)指針在 類(lèi)A和B的vtable中是一樣的,因?yàn)轭?lèi)B從類(lèi)A繼承了成員函數(shù)m2。
第25行到32行展示了你可以通過(guò)從對(duì)象的vtable讀地址的方法來(lái)調(diào)用一個(gè)虛函數(shù)。成員函數(shù)地址通過(guò)一個(gè)清楚的this指針儲(chǔ)存到了一個(gè)C類(lèi)型函數(shù)指針中了。從圖 7.25的輸出中,你可以看到它確實(shí)可以運(yùn)行。但是,請(qǐng) 不要像這樣寫(xiě)代碼!這只是用來(lái)舉例說(shuō)明虛成員函數(shù)如何使用vtable。
從這里我們可以學(xué)到一些實(shí)踐的教訓(xùn)。一個(gè)重要的事實(shí)是當(dāng)你讀或?qū)戭?lèi) 變量到一個(gè)二進(jìn)制源文件中時(shí),你必須非常小心。你不可以在整個(gè)對(duì)象中僅僅使用一個(gè)二進(jìn)制讀或?qū)?,因?yàn)榭赡軙?huì)讀或?qū)懺次募獾膙table指針! 這是一個(gè)指向留在程序內(nèi)存中的vtable的指針,而且不同的程序?qū)⒉煌M瑯拥膯?wèn)題會(huì)發(fā)生在C語(yǔ)言的結(jié)構(gòu)中,但是在C語(yǔ)言中,結(jié)構(gòu)體只有當(dāng)程序員明確將指針?lè)诺浇Y(jié)構(gòu)體中時(shí),結(jié)構(gòu)體內(nèi)部才有指針。類(lèi)A或類(lèi)B中,并沒(méi)有明顯地定義過(guò)指針。
再次,認(rèn)識(shí)到不同的編譯器實(shí)現(xiàn)虛成員函數(shù)的方法是不一樣的是非 常重要的。在In Windows中,COM(組件對(duì)象模型,Component Object Model) 類(lèi)對(duì)象使用vtable來(lái)實(shí)現(xiàn)COM接口。只有像Microsoft一樣用來(lái) 實(shí)現(xiàn)虛成員函數(shù)的編譯器才可以創(chuàng)建COM類(lèi)。這也是為什么Borland采用和Microsoft一樣的實(shí)現(xiàn)方法的原因,也是為什么不可以用gcc來(lái)創(chuàng)建COM類(lèi)的原因之一。
虛成員函數(shù)的代碼和非常虛的成員函數(shù)的代碼非常相像。只是調(diào)用它們 的代碼是不同的。如果匯編器能絕對(duì)保證調(diào)用哪個(gè)虛成員函數(shù),那么它可以忽略vtable,直接調(diào)用成員函數(shù)。( 例如 ,使用早綁定)。
更多建議: