同樣,這樣也將進行隱式類型轉換long AB::*p = &AB::B_b;。注意AB::B_b的類型為long B::,則將進行隱式類型轉換。如何轉換?原來AB::B_b映射的偏移為4,則現在將變成12+4=16,這樣才能正確執行ab.*p = 10;。
這時再回過來想剛才提的問題,AB::ABC無法區別,怎麼辦?注意還有映射元素A::ABC和B::ABC(兩個AB::ABC就是由於它們兩個而導致的),因此可以書寫ab.A::ABC();來表示調用的是映射到A::ABC的函數。這裡的A::ABC的類型是void( A:: )(),而ab是AB,因此將隱式類型轉換,則上面沒有任何語法問題(雖然說A::ABC不是結構AB的成員,但它是AB的父類的成員,C++允許這種情況,也就是說A::ABC的名字也作為類型匹配的一部分而被使用。如假設結構C也從A派生,則有C::a,但就不能書寫ab.C::a,因為從C::a的名字可以知道它並不屬於結構AB)。同樣ab.B::ABC();將調用B::ABC。注意上面結構A、B和AB都有一個成員變量名字為c且類型為long,那麼ab.c = 10;是否會如前面ab.ABC();一樣報錯?不會,因為有三個AB::c,其中有一個類型和ab的類型匹配,其映射的偏移為28,因此ab.c將會返回3028。而如果期望運用其它兩個AB::c的映射,則如上通過書寫ab.A::c和ab.B::c來偏移ab的地址以實現。
注意由於上面的說法,也就可以這樣:void( AB::*pABC )() = B::ABC; ( ab.*pABC )();。這裡的B::ABC的類型為void( B:: )(),和pABC不匹配,但正好B是AB的父類,因此將進行隱式類型轉換。如何轉換?因為B::ABC映射的是地址,而隱式類型轉換要保證在調用B::ABC之前,先將this的類型變成B*,因此要將其加12以從AB*轉變成B*。由於需要加這個12,但B::ABC又不是映射的偏移值,因此pABC實際將映射兩個數字,一個是B::ABC對應的地址,一個是偏移值12,結果pABC這個指針的長度就不再如之前所說的為4個字節,而變成了8個字節(多出來的4個字節用於記錄偏移值)。
還應注意前面在AB::ABCD中直接書寫的A_b、c、A::c等,它們實際都應該在前面加上this->,即A_b = B_b = 2;實際為this->A_b = this->B_b = 2;,則同樣如上,this被偏移了兩次以獲得正確的地址。注意上面提到的隱式類型轉換之所以會進行,是因為繼承時的權限滿足要求,否則將失敗。即如果上面AB保護繼承A而私有繼承B,則只有在AB的成員函數中可以如上進行轉換,在AB的子類的成員函數中將只能使用A的成員而不能使用B的成員,因為權限受到限制。如下將失敗。
struct AB : protected A, private B {…};
struct C : public AB { void ABCD(); };
void C::ABCD() { A_b = 10; B_b = 2; c = A::c = B::c = 24; }
這裡在C::ABCD中的B_b = 2;和B::c = 24;將報錯,因為這裡是AB的子類,而AB私有繼承自B,其子類無權將它看作B。但只是不會進行隱式類型轉換罷了,依舊可以通過顯示類型轉換來實現。而main函數中的ab.A_a = 3; ab.B_b = 4; ab.A::ABC();都將報錯,因為這是在外界發起的調用,沒有權限,不會自動進行隱式類型轉換。
注意這裡C::ABCD和AB::ABCD同名,按照上面所說,子類的成員變量都可以和父類的成員變量同名(上面AB::c和A::c及B::c同名),成員函數就更沒有問題。只用和前面一樣,按照上面所說進行類型匹配檢驗即可。應注意由於是函數,則可以參數變化而函數名依舊相同,這就成了重載函數。
虛繼承前面已經說了,當生成了AB的實例,它的長度實際應該為A的長度加B的長度再加上AB自己定義的成員所占有的長度。即AB的實例之所以又是A的實例又是B的實例,是因為一個AB的實例,它既記錄了一個A的實例又記錄了一個B的實例。則有這麼一種情況--蔬菜和水果都是植物,海洋生物和脯乳動物都是動物。即繼承的兩個父類又都從同一個類派生而來。
假設如下:
struct A { long a; };
struct B : public A { long b; }; struct C : public A { long c; };
struct D : public B, public C { long d; };
void main() { D d; d.a = 10; }
上面的B的實例就包含了一個A的實例,而C的實例也包含了一個A的實例。那麼D的實例就包含了一個B的實例和一個C的實例,則D就包含了兩個A的實例。即D定義時,將兩個父類的映射元素繼承,生成兩個映射元素,名字都為D::a,類型都為long A::,映射的偏移值也正好都為0。結果main函數中的d.a = 10;將報錯,無法確認使用哪個a。這不是很奇怪嗎?兩個映射元素的名字、類型和映射的數字都一樣!編譯器為什麼就不知道將它們定成一個,因為它們實際在D的實例中表示的偏移是不同的,一個是0一個是8。同樣,為了消除上面的問題,就書寫d.B::a = 1; d.C::a = 2;以表示不同實例中的成員a。可是B::a和C::a的類型不都是為long A::嗎?但上面說過,成員變量或成員函數它們自身的名字也將在類型匹配中起作用,因此對於d.B::a,因為左側的類型是D,則看右側,其名字表示為B,正好是D的父類,先隱式類型轉換,然後再看類型,是A,再次進行隱式類型轉換,然後返回數字。假設上面d對應的地址為3000,則d.C::a先將d這個實例轉換成C的實例,因此將3000偏移8個字節而返回long類型的地址類型的數字3008。然後再轉換成A的實例,偏移0,最後返回3008。
上面說明了一個問題,即希望從A繼承來的成員a只有一個實例,而不是像上面那樣有兩個實例。假設動物都有個饑餓度的成員變量,很明顯地鯨魚應該只需填充一個饑餓度就夠了,結果有兩個饑餓度就顯得很奇怪。對此,C++提出了虛繼承的概念。其格式就是在繼承父類時在權限語法的前面加上關鍵字virtual即可。
如下:
struct A { long a, aa, aaa; void ABC(); }; struct B : virtual public A { long b; };
這裡的B就虛繼承自A,B::b映射的偏移為多少?將不再是A的長度12,而是4。而繼承生成的3個映射元素還是和原來一樣,只是名字修飾變成B::而已,映射依舊不變。那麼為什麼B::b是4?之前的4個字節用來放什麼?上面等同於下面:
struct B { long *p; long b; long a, aa, aaa; void ABC(); };
long BDiff[] = { 0, 8 }; B::B(){ p = BDiff; }
上面的B::p指向一全局數組BDiff。什麼意思?B的實例的開頭4個字節用來記錄一個地址,也就相當於是一個指針變量,它記錄的地址所標識的內存中記錄著由於虛繼承而導致的偏移值。上面的BDiff[1]就表示要將B實例轉成A實例,就需要偏移BDiff[1]的值8,而BDiff[0]就表示要將B實例轉成B實例需要的偏移值0。為什麼還要來個B實例轉B實例?後面說明。但為什麼是數組?因為一個類可以通過多重派生而虛繼承多個類,每個類需要的偏移值都會在BDiff的數組中占一個元素,它被稱作虛類表(Virtual Class Table)。
因此當書寫B b; b.aaa = 20; long a = sizeof( b );時,a的值為20,因為多了一個4字節來記錄上面說的指針。假設b對應的地址為3000。先將B的實例轉換成A的實例,本來應該偏移12而返回3012,但編譯器發現B是虛繼承自A,則通過B::p[1]得到應該的偏移值8,然後返回3008,接著再加上B::aaa映射的8而返回3016。同樣,當b.b = 10;時,由於B::b並不是被虛繼承而來,直接將3000加上B::b映射的偏移值4得3004。而對於b.ABC();將先通過B::p[1]將b轉成A的實例然後調用A::ABC。
為什麼要像上面那樣弄得那麼麻煩?首先讓我們來了解什麼叫做虛(Virtual)。虛就是假象,並不是真的。比如一台老式電視機有10個頻道,即它最多能記住10個電視台的頻率。因此可以說1頻道是中央1台、5頻道是中央5台、7頻道是四川台。這裡就稱頻道對我們來說代表著電台頻率是虛假的,因為頻道並不是電台頻率,只是記錄了電台頻率。當我們按5頻道以換到中央5台時,有可能有人已經調過電視使得5頻道不再是中央5台,而是另一個電視台或者根本就是一片雪花沒有信號。因此虛就表示不保證,其可能正確可能錯誤,因為它一定是間接得到的,其實就相當於之前說的引用。有什麼好處?只用記著按5頻道就是中央5台,當以後不想再看中央5台而換成中央2台,則同樣的“按5頻道”卻能得到不同的結果,但是程序卻不用再編寫了,只用記著“按5頻道”就又能實現換到中央2台看。所以虛就是間接得到結果,由於間接,結果將不確定而顯得更加靈活,這在後面說明虛函數時就能看出來。但虛的壞處就是多了一道程序(要間接獲得),效率更低。
由於上面的虛繼承,導致繼承的元素都是虛的,即所有對繼承而來的映射元素的操作都應該間接獲得相應映射元素對應的偏移值或地址,但繼承的映射元素對應的偏移值或地址是不變的,為此紅字的要求就只有通過隱式類型轉換改變this的值來實現。所以上面說的B轉A需要的偏移值通過一個指針B::p來間接獲得以表現其是虛的。
因此,開始所說的鯨魚將會有兩個饑餓度就可以讓海洋生物和脯乳動物都從動物虛繼承,因此將間接使用脯乳動物和海洋生物的饑餓度這個成員,然後在派生鯨魚這個類時,讓脯乳動物和海洋生物都指向同一個動物實例(因為都是間接獲得動物的實例的,通過虛繼承來間接使用動物的成員),這樣當鯨魚填充饑餓度時,不管填充哪個饑餓度,實際都填充同一個。而C++也正好這樣做了。
如下:
struct A { long a; };
struct B : virtual public A { long b; }; struct C : virtual public A { long c; };
struct D : public B, virtual public C { long d; };
void main() { D d; d.a = 10; }
當從一個類虛繼承時,在排列派生類時(就是決定在派生類的類型定義符“{}”中定義的各成員變量的偏移值),先排列前面提到的虛類表的指針以實現間接獲取偏移值,再排列各父類,但如果父類中又有被虛繼承的父類,則先將這些部分剔除。然後排列派生類自己的映射元素。最後排列剛剛被剔除的被虛繼承的類,此時如果發現某個被虛繼承的類已經被排列過,則不用再重復排列一遍那個類,並且也不再為它生成相應的映射元素。
對於上面的B,發現虛繼承A,則先排列前面說過的B::p,然後排列A,但發現A需要被虛繼承,因此剔除,排列自己定義的映射元素B::b,映射的偏移值為4(由於B::p的占用)。最後排列A而生成繼承來的映射元素B::a,所以B的長度為12。
對於上面的D,發現要從C虛繼承,因此:
排列D::p,占4個字節。
排列父類B,發現其中的A是被虛繼承的,剔除,所以將繼承映射元素B::b(還有前面編譯器自動生成的B::p),生成D::b,占4個字節(編譯器將B::p和D::p合並為一個,後面說明虛函數時就了解了)。
排列父類C,發現C需要被虛繼承,剔除。
排列D自己定義的成員D::d,其映射的偏移值就為4+4=8,占4個字節。
排列A和C,先排列A,占4個字節,生成D::a。
排列C,先排列C中的A,結果發現它是虛繼承的,並發現已經排列過A,進而不再為C::a生成映射元素。接著排列C::p和C::c,占8個字節,生成D::c。
所以最後結構D的長度為4+4+4+4+8=24個字節,並且只有一個D::a,類型為long A::,偏移值為0。
如果上面很昏,不要緊,上面只是給出一種算法以實現虛繼承,不同的編譯器廠商會給出不同的實現方法,因此上面推得的結果對某些編譯器可能並不正確。不過應記住虛繼承的含義--被虛繼承的類的所有成員都必須被間接獲得,至於如何間接獲得,則不同的
編譯器有不同的處理方式。
由於需要保證間接獲得,所以對於long D::*pa = &D::a;,由於是long D::*,編譯器發現D的繼承體系中存在虛繼承,必須要保證其某些成員的間接獲得,因此pa中放的將不再是偏移值,否則d.*pa = 10;將導致直接獲得偏移值(將pa的內容取出來即可),違反了虛繼承的含義。為了要間接訪問pa所記錄的偏移值,則必須保證代碼執行時,當pa裡面放的是D::a時會間接,而D::d時則不間接。很明顯,這要更多和更復雜的代碼,大多數編譯器對此的處理就是全部都使用間接獲得。因此pa的長度將為8字節,其中一個4字節記錄偏移,還有一個4字節記錄一個序號。這個序號則用於前面說的虛類表以獲得正確的因虛繼承而導致的偏移量。因此前面的B::p所指的第一個元素的值表示B實例轉換成B實例,是為了在這裡實現全部間接獲得而提供的。
注意上面的D::p對於不同的D的實例將不同,只不過它們的內容都相同(都是結構D的虛類表的地址)。當D的實例剛剛生成時,那個實例的D::p的值將是一隨機數。為了保證D::p被正確初始化,上面的結構D雖然沒有生成構造函數,但編譯器將自動為D生成一缺省構造函數(沒有參數的構造函數)以保證D::p和上面從C繼承來的C::p的正確初始化,結果將導致D d = { 23, 4 };錯誤,因為D已經定義了一個構造函數,即使沒有在代碼上表現出來。
那麼虛繼承有什麼意義呢?它從功能上說是間接獲得虛繼承來的實例,從類型上說與普通的繼承沒有任何區別,即虛繼承和前面的public等一樣,只是一個語法上的提供,對於數字的類型沒有任何影響。在了解它的意義之前先看下虛函數的含義。