1.多重繼承
一個例子
| 123456789101112131415161718192021222324252627282930313233 | #include <iostream>using namespace std; class Base1{public: virtual void foo1() {};}; class Base2{public: virtual void foo2() {};}; class MI : public Base1,public Base2{public: virtual void foo1 () {cout << "MI::foo1" << endl;} virtual void foo2 () {cout << "MI::foo2" << endl;}}; int main(){ MI oMI; Base1* pB1 = &oMI; pB1->foo1(); Base2* pB2 = (Base2*)(pB1); // 指針強(qiáng)行轉(zhuǎn)換,沒有偏移 pB2->foo2(); pB2 = dynamic_cast<Base2*>(pB1);// 指針動態(tài)轉(zhuǎn)換,dynamic_cast幫你偏移 pB2->foo2(); return 0;} |
你會認(rèn)為屏幕上會輸出什么?是下面的結(jié)果嗎?
MI::foo1 MI::foo2 MI::foo2 |
這樣認(rèn)為沒有什么不對的,因?yàn)镃++的多態(tài)性保證用父類指針可以正確的找到子類實(shí)現(xiàn),并調(diào)用。所以會有上面的輸出。
但是,現(xiàn)實(shí)卻不是這樣,下面是真實(shí)的輸出:

為什么會出現(xiàn)上面的情況呢,上面代碼中的注釋部分也許解釋了,這里再來詳細(xì)的來分析一下。
首先,C++使用一種稱之為vtable(google “vtable” for more details)的東西實(shí)現(xiàn)virtual函數(shù)多態(tài)調(diào)用。vtable每個類中都有一個,該類的所有對象公用,由編譯器幫你生成,只要有virtual函數(shù)的類,均會有vtable。在繼承過程中,由于類Base1和類Base2都有vtable,所以類MI繼承了兩個vtable。簡單的分析一下對象oMI內(nèi)存結(jié)構(gòu),如下:
0 vtable_address_for_Base1 –> [MI::foo1, NULL] 4 vtable_address_for_Base2 –> [MI::foo2, NULL] |
其實(shí)很簡單,就兩個vtable的指針,0和4代表相對地址,指針地址大小為4。
pB1的值為0(pB1 == 0),所以調(diào)用“pB1->foo1()”時,可以正確的找到MI::fool這個函數(shù)執(zhí)行。
但是當(dāng)使用強(qiáng)行轉(zhuǎn)換,將pB1轉(zhuǎn)給pB2,那么實(shí)質(zhì)上pB2的值也是0(pB2 == 0),當(dāng)調(diào)用“pB2->foo2()”時,無法在第一個vtalbe中找到對應(yīng)的函數(shù),但是卻不報錯,而是選擇執(zhí)行函數(shù)MI::foo1,不知道為什么會有這種行為,但是這種行為卻十分惡心,導(dǎo)致結(jié)果無法預(yù)期的(最后調(diào)用的函數(shù)會與函數(shù)申明的循序有關(guān)),不太會引起注意,使得bug十分隱晦。
可以設(shè)想,當(dāng)一個有復(fù)雜的業(yè)務(wù)邏輯的程序,而類似這種函數(shù)調(diào)用和指針強(qiáng)行轉(zhuǎn)換分布在不同的函數(shù)或模塊中,可想而知,bug定位十分困難。
當(dāng)使用動態(tài)轉(zhuǎn)換時,也就是“pB2 = dynamic_cast<Base2*>(pB1)”,dynamic_cast函數(shù)會根據(jù)尖括號中的類型進(jìn)行指針偏移,所以pB2的值為4(pB2 == 4),這樣調(diào)用“pB2->foo2()”就會按照期望的方式執(zhí)行。
結(jié)論
上面的現(xiàn)象在單繼承中是不會出現(xiàn)的,因?yàn)橹挥幸粋€vtable(子類的virtual函數(shù)會自動追加到第一個父類的vtable的結(jié)尾)。所以不會出現(xiàn)上面的現(xiàn)象,而多重繼承卻出現(xiàn)了上面的想象,所以需要注意以下兩點(diǎn):
1. 多重繼承需要慎用
2. 類型轉(zhuǎn)換盡量采用c++內(nèi)置的類型轉(zhuǎn)換函數(shù),而不要強(qiáng)行轉(zhuǎn)換
總結(jié):虛繼承,虛函數(shù)都會產(chǎn)生相關(guān)vptrvptr 帶有到基類地址的偏移量通過 dynamic_cast 可以實(shí)現(xiàn)多繼承不同父類之間的轉(zhuǎn)換。
2.虛基類
教科書上面對C++虛基類的描述玄而又玄,名曰“共享繼承”,名曰“各派生類的對象共享基類的的一個拷貝”,其實(shí)說白了就是解決多重多級繼承造成的二義性問題。例如有基類B,從B派生出C和D,然后類F又同時繼承了C和D,現(xiàn)在類F的一個對象里面包含了兩個基類B的對象,如果F訪問自己的從基類B那里繼承過來的的數(shù)據(jù)成員或者函數(shù)成員那么編譯器就不知道你指的到底是從C那里繼承過來的B對象呢還是從D那里繼承過來的B對象。
于是虛基類誕生了,將C和D的繼承方式改為虛繼承,那么F訪問自己從B那里繼承過來的成員就不會有二義性問題了,也就是將F對象里的B對象統(tǒng)一為一個,只有一個基類B對象,下面是一段代碼說明了對虛基類的使用。
#include <iostream>using namespace std;class A{ public: int i; void showa(){cout<<"i="<<i<<endl;}};class B:virtual public A //此處采用虛繼承{ public: int j;};class C:virtual public A //此處采用虛繼承{ public: int k;};class D:public B,public C{ public: int m;};int main(){ A a; B b; C c; a.i=1; a.showa(); b.i=2; b.showa(); c.i=3; c.showa(); D d; d.i=4; d.showa(); //cout << "Hello world!" << endl; return 0;}從這個代碼我們可以看出B,C,D從A那里繼承過來了i這個變量并且它們之間不會有任何影響,如果B和C不是虛繼承方式的,那么d.i=4;就不能編譯通過了。
3.虛函數(shù)和純虛函數(shù)
1. 虛函數(shù)和純虛函數(shù)可以定義在同一個類(class)中,含有純虛函數(shù)的類被稱為抽象類(abstract class),而只含有虛函數(shù)的類(class)不能被稱 $
2. 虛函數(shù)可以被直接使用,也可以被子類(sub class)重載以后以多態(tài)的形式調(diào)用,而純虛函數(shù)必須在子類(sub class)中實(shí)現(xiàn)該函數(shù)才可以使用 $
只有聲明而沒有定義。
3. 虛函數(shù)和純虛函數(shù)都可以在子類(sub class)中被重載,以多態(tài)的形式被調(diào)用。
4. 虛函數(shù)和純虛函數(shù)通常存在于抽象基類(abstract base class -ABC)之中,被繼承的子類重載,目的是提供一個統(tǒng)一的接口。
5. 虛函數(shù)的定義形式:virtual {method body}
純虛函數(shù)的定義形式:virtual { } = 0;
在虛函數(shù)和純虛函數(shù)的定義中不能有static標(biāo)識符,原因很簡單,被static修飾的函數(shù)在編譯時候要求前期bind,然而虛函數(shù)卻是動態(tài)綁定(run-ti$
6. 如果一個類中含有純虛函數(shù),那么任何試圖對該類進(jìn)行實(shí)例化的語句都將導(dǎo)致錯誤的產(chǎn)生,因?yàn)槌橄蠡悾ˋBC)是不能被直接調(diào)用的。必須被子類 $
以下為一個簡單的虛函數(shù)和純虛寒?dāng)?shù)的使用演示!
|
新聞熱點(diǎn)
疑難解答
圖片精選