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