C++虚继承(七) --- 虚继承对基类构造函数调用顺序的影响
繼承作為面向?qū)ο缶幊痰囊环N基本特征,其使用頻率非常高。而繼承包含了虛擬繼承和普通繼承,在可見性上分為public、protected、private。可見性繼承比較簡單,而虛擬繼承對學(xué)習(xí)c++的難度較大。
首先,虛擬繼承與普通繼承的區(qū)別有:
假設(shè)derived 繼承自base類,那么derived與base是一種“is a”的關(guān)系,即derived類是base類,而反之錯誤;
假設(shè)derived 虛繼承自base類,那么derivd與base是一種“has a”的關(guān)系,即derived類有一個(gè)指向base類的vptr。(貌似有些牽強(qiáng)!某些編譯器確實(shí)如此,關(guān)于虛繼承與普通繼承的差異見:c++ 虛繼承與繼承的差異?)
因此虛繼承可以認(rèn)為不是一種繼承關(guān)系,而可以認(rèn)為是一種組合的關(guān)系。正是因?yàn)檫@樣的區(qū)別,下面我們針對虛擬繼承來具體分析。虛擬繼承中遇到最廣泛的是菱形結(jié)構(gòu)。下面從菱形虛繼承結(jié)構(gòu)說起吧:
[cpp]?view plaincopy
程序運(yùn)行的輸出結(jié)果為:
stream::stream()!
istream::istream()!
ostream::ostream()!
iiostream::iiostream()!???
輸出這樣的結(jié)果是毫無懸念的!本來虛擬繼承的目的就是當(dāng)多重繼承出現(xiàn)重復(fù)的基類時(shí),其只保存一份基類。減少內(nèi)存開銷。其繼承結(jié)構(gòu)為:
? ? ? ? ? ??stream?
???????????/??? ? ? ? ? ???\???
?????istream???ostream???
???????????\??? ? ? ? ? ? ???/
???????????iiostream??
這樣子的菱形結(jié)構(gòu),使公共基類只產(chǎn)生一個(gè)拷貝。
而現(xiàn)在我們換種方式使用虛繼承:
[cpp]?view plaincopy
其輸出結(jié)果為:
stream::stream()!
istream::istream()!
stream::stream()!
ostream::ostream()!
iiostream::iiostream()!
從結(jié)果可以看到,其構(gòu)造過程中重復(fù)出現(xiàn)基類stream的構(gòu)造過程。這樣就完全沒有達(dá)到虛擬繼承的目的。其繼承結(jié)構(gòu)為:
stream????????? ? ??stream????????????????????????????????????????????????????????????????????
????????????\????????? ? ? ? ? ???/???????????????????????????????
???istream????ostream??????????????????????????????????????
?????????????\?????? ? ? ? ? ????/?????????????????????????????????????????????????????????????
??????????????iiostream??
從繼承結(jié)構(gòu)可以看出,如果iiostream對象調(diào)用基類stream中的成員方法,會導(dǎo)致方法的二義性。因?yàn)?span style="line-height:21px">iiostream含有指向其虛繼承基類?istream,ostream的vptr。而?istream,ostream包含了stream的空間,所以導(dǎo)致iiostream不知道導(dǎo)致是調(diào)用那個(gè)stream的方法。要解決改問題,可以指定vptr,即在調(diào)用成員方法是需要加上作用域,例如
[cpp]?view plaincopy
編譯器提示調(diào)用f方法錯誤。而采用
[cpp]?view plaincopy
編譯通過,并且會調(diào)用istream類vptr指向的f()方法。 前面說了這么多,在實(shí)際的應(yīng)用中虛擬繼承的胡亂使用,更是會導(dǎo)致繼承順序以及基類構(gòu)造順序的混亂。如下面的代碼:
[cpp]?view plaincopy
上面的代碼是來自《Exceptional C++ Style》中關(guān)于繼承順序的一段代碼。可以看到,上面的代碼繼承關(guān)系非常復(fù)雜,而且層次不是特別的清楚。而虛繼承的加入更是讓繼承結(jié)構(gòu)更加無序。不管怎么樣,我們還是可以根據(jù)c++的標(biāo)準(zhǔn)來分析上面代碼的構(gòu)造順序。c++對于創(chuàng)建一個(gè)類類型的初始化順序是這樣子的:
1.最上層派生類的構(gòu)造函數(shù)負(fù)責(zé)調(diào)用虛基類子對象的構(gòu)造函數(shù)。所有虛基類子對象會按照深度優(yōu)先、從左到右的順序進(jìn)行初始化;
2.直接基類子對象按照它們在類定義中聲明的順序被一一構(gòu)造起來;
3.非靜態(tài)成員子對象按照它們在類定義體中的聲明的順序被一一構(gòu)造起來;
4.最上層派生類的構(gòu)造函數(shù)體被執(zhí)行。
根據(jù)上面的規(guī)則,可以看出,最先構(gòu)造的是虛繼承基類的構(gòu)造函數(shù),并且是按照深度優(yōu)先,從左往右構(gòu)造。因此,我們需要將繼承結(jié)構(gòu)劃分層次。顯然上面的代碼可以認(rèn)為是4層繼承結(jié)構(gòu)。其中最頂層的是B1,B2類。第二層是V1,V2,V3。第三層是D1,D2.最底層是X。而D1虛繼承V1,D2虛繼承V2,且D1和D2在同一層。所以V1最先構(gòu)造,其次是V2.在V2構(gòu)造順序中,B1先于B2.虛基類構(gòu)造完成后,接著是直接基類子對象構(gòu)造,其順序?yàn)镈1,D2.最后為成員子對象的構(gòu)造,順序?yàn)槁暶鞯捻樞颉?gòu)造完畢后,開始按照構(gòu)造順序執(zhí)行構(gòu)造函數(shù)體了。所以其最終的輸出結(jié)果為:
B1::B1()!<
V1::V1()!<
B1::B1()!<
B2::B2()!<
V2::V2()!<
D1::D1()!<
B3::B3()!<
D2::D2()!<
M1::M1()!<
M2::M2()!<
從結(jié)果也可以看出其構(gòu)造順序完全符合上面的標(biāo)準(zhǔn)。而在結(jié)果中,可以看到B1重復(fù)構(gòu)造。還是因?yàn)闆]有按照要求使用virtual繼承導(dǎo)致的結(jié)果。要想只構(gòu)造B1一次,可以將virtual全部改在B1上,如下面的代碼:
[cpp]?view plaincopy
根據(jù)上面的代碼,其輸出結(jié)果為:
B1::B1()!<
V1::V1()!<
D1::D1()!<
B2::B2()!<
V2::V2()!<
B3::B3()!<
D2::D2()!<
M1::M1()!<
M2::M2()!<
由于虛繼承導(dǎo)致其構(gòu)造順序發(fā)生比較大的變化。不管怎么,分析的規(guī)則還是一樣。
上面分析了這么多,我們知道了虛繼承有一定的好處,但是虛繼承會增大占用的空間。這是因?yàn)槊恳淮翁摾^承會產(chǎn)生一個(gè)vptr指針。空間因素在編程過程中,我們很少考慮,而構(gòu)造順序卻需要小心,因此使用未構(gòu)造對象的危害是相當(dāng)大的。因此,我們需要小心的使用繼承,更要確保在使用繼承的時(shí)候保證構(gòu)造順序不會出錯。下面我再著重強(qiáng)調(diào)一下基類的構(gòu)造順序規(guī)則:
1.最上層派生類的構(gòu)造函數(shù)負(fù)責(zé)調(diào)用虛基類子對象的構(gòu)造函數(shù)。所有虛基類子對象會按照深度優(yōu)先、從左到右的順序進(jìn)行初始化;
2.直接基類子對象按照它們在類定義中聲明的順序被一一構(gòu)造起來;
3.非靜態(tài)成員子對象按照它們在類定義體中的聲明的順序被一一構(gòu)造起來;
4.最上層派生類的構(gòu)造函數(shù)體被執(zhí)行。總結(jié)
以上是生活随笔為你收集整理的C++虚继承(七) --- 虚继承对基类构造函数调用顺序的影响的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++虚继承(六) --- 虚继承浅析
- 下一篇: C++虚继承(八) --- 虚继承与继