同樣,這樣也將進行隱式類型轉換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等一樣,只是一個文法上的提供,對於數位類型沒有任何影響。在瞭解它的意義之前先看下虛函數的含義。