C++虛繼承與虛基類的本質


 

我的新浪微博:http://weibo.com/freshairbrucewoo

 

歡迎大家相互交流,共同提高技術。

 

 聲明:此篇博客網上轉載,轉載原因是對於虛擬繼承分析得比較徹底:讓我很好的理解了虛擬繼承的作用於帶來的壞處(性能的損失)。 

  虛繼承和虛基類的定義是非常的簡單的,同時也是非常容易判斷一個繼承是否是虛繼承的,雖然這兩個概念的定義是非常的簡單明確的,但是在C++語言中虛繼承作為一個比較生僻的但是又是絕對必要的組成部份而存在着,並且其行為和模型均表現出和一般的繼承體系之間的巨大的差異(包括訪問性能上的差異),現在我們就來徹底的從語言、模型、性能和應用等多個方面對虛繼承和虛基類進行研究。 首先還是先給出虛繼承和虛基類的定義。 虛繼承:在繼承定義中包含了virtual關鍵字的繼承關系; 虛基類:在虛繼承體系中的通過virtual繼承而來的基類,需要注意的是: struct CSubClass : public virtual CBase {}; 其中CBase稱之為CSubClass 的虛基類,而不是說CBase就是個虛基類,因為CBase還可以不不是虛繼承體系 中的基類。 有了上面的定義后,就可以開始虛繼承和虛基類的本質研究了,下面按照語法、語義、 模型、性能和應用五個方面進行全面的描述。
    1. 語法 語法有語言的本身的定義所決定,總體上來說非常的簡單,如下: struct CSubClass : public virtual CBaseClass {}; 其中可以采用public、protected、private三種不同的繼承關鍵字進行修飾,只要 確保包含virtual就可以了,這樣一來就形成了虛繼承體系,同時CBaseClass就成為 了CSubClass的虛基類了。 其實並沒有那么的簡單,如果出現虛繼承體系的進一步繼承會出現什么樣的狀況呢? 如下所示:

 1  /* * 帶有數據成員的基類 */
2 struct CBaseClass1
3 {
4
5 CBaseClass1( size_t i ) : m_val( i ) {}
6 size_t m_val;
7 };
8 /* * 虛擬繼承體系 */
9 struct CSubClassV1 : public virtual CBaseClass1
10 {
11 CSubClassV1( size_t i ) : CBaseClass1( i ) {}
12 };
13 struct CSubClassV2 : public virtual CBaseClass1
14 {
15 CSubClassV2( size_t i ) : CBaseClass1( i ) {}
16 };
17 struct CDiamondClass1 : public CSubClassV1, public CSubClassV2
18 {
19 CDiamondClass1( size_t i ) : CBaseClass1( i ), CSubClassV1( i ), CSubClassV2( i ) {}
20 };
21 struct CDiamondSubClass1 : public CDiamondClass1
22 {
23 CDiamondSubClass1( size_t i ) : CBaseClass1( i ), CDiamondClass1( i ) {}
24 };

  注意上面代碼中的CDiamondClass1和CDiamondSubClass1兩個類的構造函數初始化列 表中的內容。可以發現其中均包含了虛基類CBaseClass1的初始化工作,如果沒有這 個初始化語句就會導致編譯時錯誤,為什么會這樣呢?一般情況下不是只要在 CSubClassV1和CSubClassV2中包含初始化就可以了么?要解釋該問題必須要明白虛 繼承的語義特征,所以參看下面語義部分的解釋。
  2. 語義 從語義上來講什么是虛繼承和虛基類呢?上面僅僅是從如何在C++語言中書寫合法的 虛繼承類定義而已。首先來了解一下virtual這個關鍵字在C++中的公共含義,在C++ 語言中僅僅有兩個地方可以使用virtual這個關鍵字,一個就是類成員虛函數和這里 所討論的虛繼承。不要看這兩種應用場合好像沒什么關系,其實他們在背景語義上 具有virtual這個詞所代表的共同的含義,所以才會在這兩種場合使用相同的關鍵字。 那么virtual這個詞的含義是什么呢?

virtual在《美國傳統詞典[雙解]》中是這樣定義的:

  adj.(形容詞) 1. Existing or resulting in essence or effect though not in actual fact, form, or name: 實質上的,實際上的:雖然沒有實際的事實、形式或名義,但在實際上或效 果上存在或產生的;

           2. Existing in the mind, especially as a product of the imagination. Used in literary criticism of text. 虛的,內心的:在頭腦中存在的,尤指意想的產物。用於文學批評中。

   我們采用第一個定義,也就是說被virtual所修飾的事物或現象在本質上是存在的, 但是沒有直觀的形式表現,無法直接描述或定義,需要通過其他的間接方式或手段 才能夠體現出其實際上的效果。 那么在C++中就是采用了這個詞意,不可以在語言模型中直接調用或體現的,但是確 實是存在可以被間接的方式進行調用或體現的。比如:虛函數必須要通過一種間接的 運行時(而不是編譯時)機制才能夠激活(調用)的函數,而虛繼承也是必須在運行 時才能夠進行定位訪問的一種體制。存在,但間接。其中關鍵就在於存在、間接和共 享這三種特征。 對於虛函數而言,這三個特征是很好理解的,間接性表明了他必須在運行時根據實際 的對象來完成函數尋址,共享性表象在基類會共享被子類重載后的虛函數,其實指向 相同的函數入口。 對於虛繼承而言,這三個特征如何理解呢?存在即表示虛繼承體系和虛基類確實存在, 間接性表明了在訪問虛基類的成員時同樣也必須通過某種間接機制來完成(下面模型 中會講到),共享性表象在虛基類會在虛繼承體系中被共享,而不會出現多份拷貝。 那現在可以解釋語法小節中留下來的那個問題了,“為什么一旦出現了虛基類,就必須在沒有一個繼承類中都必須包含虛基類的初始化語句”。由上面的分析可以知道, 虛基類是被共享的,也就是在繼承體系中無論被繼承多少次,對象內存模型中均只會 出現一個虛基類的子對象(這和多繼承是完全不同的),這樣一來既然是共享的那么每一個子類都不會獨占,但是總還是必須要有一個類來完成基類的初始化過程(因為 所有的對象都必須被初始化,哪怕是默認的),同時還不能夠重復進行初始化,那到 底誰應該負責完成初始化呢?C++標准中(也是很自然的)選擇在每一次繼承子類中 都必須書寫初始化語句(因為每一次繼承子類可能都會用來定義對象),而在最下層 繼承子類中實際執行初始化過程。所以上面在每一個繼承類中都要書寫初始化語句, 但是在創建對象時,而僅僅會在創建對象用的類構造函數中實際的執行初始化語句, 其他的初始化語句都會被壓制不調用。
  3. 模型 為了實現上面所說的三種語義含義,在考慮對象的實現模型(也就是內存模型)時就 很自然了。在C++中對象實際上就是一個連續的地址空間的語義代表,我們來分析虛 繼承下的內存模型。

  3.1. 存在 也就是說在對象內存中必須要包含虛基類的完整子對象,以便能夠完成通過地址 完成對象的標識。那么至於虛基類的子對象會存放在對象的那個位置(頭、中間、 尾部)則由各個編譯器選擇,沒有差別。(在VC8中無論虛基類被聲明在什么位置, 虛基類的子對象都會被放置在對象內存的尾部)

  3.2. 間接 間接性表明了在直接虛基承子類中一定包含了某種指針(偏移或表格)來完成通 過子類訪問虛基類子對象(或成員)的間接手段(因為虛基類子對象是共享的, 沒有確定關系),至於采用何種手段由編譯器選擇。(在VC8中在子類中放置了 一個虛基類指針vbc,該指針指向虛函數表中的一個slot,該slot中存放則虛基 類子對象的偏移量的負值,實際上就是個以補碼表示的int類型的值,在計算虛 基類子對象首地址時,需要將該偏移量取絕對值相加,這個主要是為了和虛表 中只能存放虛函數地址這一要求相區別,因為地址是原碼表示的無符號int類型 的值)

  3.3. 共享 共享表明了在對象的內存空間中僅僅能夠包含一份虛基類的子對象,並且通過 某種間接的機制來完成共享的引用關系。在介紹完整個內容后會附上測試代碼, 體現這些內容。

  4. 性能 由於有了間接性和共享性兩個特征,所以決定了虛繼承體系下的對象在訪問時必然會在時間和空間上與一般情況有較大不同。

  4.1. 時間 在通過繼承類對象訪問虛基類對象中的成員(包括數據成員和函數成員)時,都 必須通過某種間接引用來完成,這樣會增加引用尋址時間(就和虛函數一樣), 其實就是調整this指針以指向虛基類對象,只不過這個調整是運行時間接完成的。 (在VC8中通過打開匯編輸出,可以查看*.cod文件中的內容,在訪問虛基類對象 成員時會形成三條mov間接尋址語句,而在訪問一般繼承類對象時僅僅只有一條mov 常量直接尋址語句)

  4.2. 空間 由於共享所以不同在對象內存中保存多份虛基類子對象的拷貝,這樣較之多繼承節省空間。

  5. 應用 談了那么多語言特性和內容,那么在什么情況下需要使用虛繼承,而一般應該如何使 用呢? 這個問題其實很難有答案,一般情況下如果你確性出現多繼承沒有必要,必須要共享 基類子對象的時候可以考慮采用虛繼承關系(C++標准ios體系就是這樣的)。由於每 一個繼承類都必須包含初始化語句而又僅僅只在最底層子類中調用,這樣可能就會使 得某些上層子類得到的虛基類子對象的狀態不是自己所期望的(因為自己的初始化語 句被壓制了),所以一般建議不要在虛基類中包含任何數據成員(不要有狀態),只 可以作為接口類來提供。
附錄:測試代碼

  1 #include #include 
2 /* * 帶有數據成員的基類 */
3 struct CBaseClass1
4 {
5 CBaseClass1( size_t i ) : m_val( i ) {}
6 size_t m_val;
7 };
8 /* * 虛擬繼承體系 */
9 struct CSubClassV1 : public virtual CBaseClass1
10 {
11 CSubClassV1( size_t i ) : CBaseClass1( i ) {}
12 };
13 struct CSubClassV2 : public virtual CBaseClass1
14 {
15 CSubClassV2( size_t i ) : CBaseClass1( i ) {}
16 };
17 struct CDiamondClass1 : public CSubClassV1, public CSubClassV2
18 {
19 CDiamondClass1( size_t i ) : CBaseClass1( i ), CSubClassV1( i ), CSubClassV2( i ) {}
20 };
21 struct CDiamondSubClass1 : public CDiamondClass1
22 {
23 CDiamondSubClass1( size_t i ) : CBaseClass1( i ), CDiamondClass1( i ) {}
24 };
25 /* * 正常繼承體系 */
26 struct CSubClassN1 : public CBaseClass1
27 {
28 CSubClassN1( size_t i ) : CBaseClass1( i ) {}
29 };
30 struct CSubClassN2 : public CBaseClass1
31 {
32 CSubClassN2( size_t i ) : CBaseClass1( i ) {}
33 };
34 struct CMultiClass1 : public CSubClassN1, public CSubClassN2
35 {
36 CMultiClass1( size_t i ) : CSubClassN1( i ), CSubClassN2( i ) {}
37 };
38 struct CMultiSubClass1 : public CMultiClass1
39 {
40 CMultiSubClass1( size_t i ) : CMultiClass1( i ) {}
41 };
42 /* * 不帶有數據成員的接口基類 */
43 struct CBaseClass2
44 {
45 virtual void func() {};
46 virtual ~CBaseClass2() {}
47 };
48 /* * 虛擬繼承體系 */ //
49 struct CBaseClassX
50 {
51 CBaseClassX()
52 {
53 i1 = i2 = 0xFFFFFFFF;
54 }
55 size_t i1, i2;
56 };
57 struct CSubClassV3 : public virtual CBaseClass2
58 { };
59 struct CSubClassV4 : public virtual CBaseClass2
60 { };
61 struct CDiamondClass2 : public CSubClassV3, public CSubClassV4
62 { };
63 struct CDiamondSubClass2 : public CDiamondClass2
64 { };
65 /* * 正常繼承體系 */
66 struct CSubClassN3 : public CBaseClass2
67 { };
68 struct CSubClassN4 : public CBaseClass2
69 { };
70 struct CMultiClass2 : public CSubClassN3, public CSubClassN4
71 { };
72 struct CMultiSubClass2 : public CMultiClass2
73 { };
74 /* * 內存布局用類聲明. */
75 struct CLayoutBase1
76 {
77 CLayoutBase1() : m_val1( 0 ), m_val2( 1 ) {}
78 size_t m_val1, m_val2;
79 };
80 struct CLayoutBase2
81 {
82 CLayoutBase2() : m_val1( 3 ) {}
83 size_t m_val1;
84 };
85 struct CLayoutSubClass1 : public virtual CBaseClass1, public CLayoutBase1, public CLayoutBase2
86 {
87 CLayoutSubClass1() : CBaseClass1( 2 ) {}
88 };
89 #define MAX_TEST_COUNT 1000 * 1000 * 16
90 #define TIME_ELAPSE() ( std::clock() - start * 1.0 ) / CLOCKS_PER_SEC
91 int main( int argc, char *argv[] )
92 {
93 /* * 類體系中的尺寸. */ std::cout << "================================ sizeof ================================" << std::endl; std::cout << " ----------------------------------------------------------------" << std::endl; std::cout << "sizeof( CBaseClass1 ) = " << sizeof( CBaseClass1 ) << std::endl; std::cout << std::endl; std::cout << "sizeof( CSubClassV1 ) = " << sizeof( CSubClassV1 ) << std::endl; std::cout << "sizeof( CSubClassV2 ) = " << sizeof( CSubClassV2 ) << std::endl; std::cout << "sizeof( CDiamondClass1 ) = " << sizeof( CDiamondClass1 ) << std::endl; std::cout << "sizeof( CDiamondSubClass1 ) = " << sizeof( CDiamondSubClass1 ) << std::endl; std::cout << std::endl; std::cout << "sizeof( CSubClassN1 ) = " << sizeof( CSubClassN1 ) << std::endl; std::cout << "sizeof( CSubClassN2 ) = " << sizeof( CSubClassN2 ) << std::endl; std::cout << "sizeof( CMultiClass1 ) = " << sizeof( CMultiClass1 ) << std::endl; std::cout << "sizeof( CMultiSubClass1 ) = " << sizeof( CMultiSubClass1 ) << std::endl;
94 std::cout << " ----------------------------------------------------------------" << std::endl; std::cout << "sizeof( CBaseClass2 ) = " << sizeof( CBaseClass2 ) << std::endl; std::cout << std::endl; std::cout << "sizeof( CSubClassV3 ) = " << sizeof( CSubClassV3 ) << std::endl; std::cout << "sizeof( CSubClassV4 ) = " << sizeof( CSubClassV4 ) << std::endl; std::cout << "sizeof( CDiamondClass2 ) = " << sizeof( CDiamondClass2 ) << std::endl; std::cout << "sizeof( CDiamondSubClass2 ) = " << sizeof( CDiamondSubClass2 ) << std::endl; std::cout << std::endl; std::cout << "sizeof( CSubClassN3 ) = " << sizeof( CSubClassN3 ) << std::endl; std::cout << "sizeof( CSubClassN4 ) = " << sizeof( CSubClassN4 ) << std::endl; std::cout << "sizeof( CMultiClass2 ) = " << sizeof( CMultiClass2 ) << std::endl; std::cout << "sizeof( CMultiSubClass2 ) = " << sizeof( CMultiSubClass2 ) << std::endl; /* * 對象內存布局 */ std::cout << "================================ layout ================================" << std::endl; std::cout << " --------------------------------MI------------------------------" << std::endl; CLayoutSubClass1 *lsc = new CLayoutSubClass1; std::cout << "sizeof( CLayoutSubClass1 ) = " << sizeof( CLayoutSubClass1 ) << std::endl; std::cout << "CLayoutBase1 offset of CLayoutSubClass1 is " << (char*)(CLayoutBase1 *)lsc - (char*)lsc << std::endl; std::cout << "CBaseClass1 offset of CLayoutSubClass1 is " << (char*)(CBaseClass1 *)lsc - (char*)lsc << std::endl; std::cout << "CLayoutBase2 offset of CLayoutSubClass1 is " << (char*)(CLayoutBase2 *)lsc - (char*)lsc << std::endl;
95 int *ptr = (int*)lsc; std::cout << "vbc in CLayoutSubClass1 is " << *(int*)ptr[3] << std::endl;
96 delete lsc;
97 std::cout << " --------------------------------SI------------------------------" << std::endl; CSubClassV1 *scv1 = new CSubClassV1( 1 ); std::cout << "sizeof( CSubClassV1 ) = " << sizeof( CSubClassV1 ) << std::endl; std::cout << "CBaseClass1 offset of CSubClassV1 is " << (char*)(CBaseClass1 *)scv1 - (char*)scv1 << std::endl;
98 ptr = (int*)scv1; std::cout << "vbc in CSubClassV1 is " << *(int*)ptr[0] << std::endl;
99 delete scv1;
100 /* * 性能測試 */ std::cout << "================================ Performance ================================" << std::endl; double times[4]; size_t idx = 0;
101 CSubClassV1 *ptr1 = new CDiamondClass1( 1 ); std::clock_t start = std::clock(); { for ( size_t i = 0; i < MAX_TEST_COUNT; ++i ) ptr1->m_val = i; } times[idx++] = TIME_ELAPSE(); delete static_cast( ptr1 );
102 CSubClassN1 *ptr2 = new CMultiClass1( 0 ); start = std::clock(); { for ( size_t i = 0; i < MAX_TEST_COUNT; ++i ) ptr2->m_val = i; } times[idx++] = TIME_ELAPSE(); delete static_cast( ptr2 );
103 std::cout << "CSubClassV1::ptr1->m_val " << times[0] << " s" << std::endl; std::cout << "CSubClassN1::ptr2->m_val " << times[1] << " s" << std::endl;
104 return 0; }

 

測試環境: 軟件環境:Visual Studio2005 Pro + SP1, boost1.34.0 硬件環境:PentiumD 3.0GHz, 4G RAM 測試數據: ================================ sizeof ================================ ---------------------------------------------------------------- sizeof( CBaseClass1 )       = 4
sizeof( CSubClassV1 )       = 8 sizeof( CSubClassV2 )       = 8 sizeof( CDiamondClass1 )    = 12 sizeof( CDiamondSubClass1 ) = 12
sizeof( CSubClassN1 )       = 4 sizeof( CSubClassN2 )       = 4 sizeof( CMultiClass1 )      = 8 sizeof( CMultiSubClass1 )   = 8 ---------------------------------------------------------------- sizeof( CBaseClass2 )       = 4
sizeof( CSubClassV3 )       = 8 sizeof( CSubClassV4 )       = 8 sizeof( CDiamondClass2 )    = 12 sizeof( CDiamondSubClass2 ) = 12
sizeof( CSubClassN3 )       = 4 sizeof( CSubClassN4 )       = 4 sizeof( CMultiClass2 )      = 8 sizeof( CMultiSubClass2 )   = 8 ================================ layout ================================ --------------------------------MI------------------------------ sizeof( CLayoutSubClass1 )   = 20 CLayoutBase1 offset of CLayoutSubClass1 is 0 CBaseClass1  offset of CLayoutSubClass1 is 16 CLayoutBase2 offset of CLayoutSubClass1 is 8 vbc in CLayoutSubClass1 is -12 --------------------------------SI------------------------------ sizeof( CSubClassV1 )   = 8 CBaseClass1 offset of CSubClassV1 is 4 vbc in CSubClassV1 is 0 ================================ Performance ================================ CSubClassV1::ptr1->m_val 0.062 s CSubClassN1::ptr2->m_val 0.016 s
結果分析:

1. 由於虛繼承引入的間接性指針所以導致了虛繼承類的尺寸會增加4個字節;

2. 由Layout輸出可以看出,虛基類子對象被放在了對象的尾部(偏移為16),並且vbc 指針必須緊緊的接在虛基類子對象的前面,所以vbc指針所指向的內容為“偏移 - 4”;

3. 由於VC8將偏移放在了虛函數表中,所以為了區分函數地址和偏移,所以偏移是用補 碼int表示的負值;

4. 間接性可以通過性能來看出,在虛繼承體系同通過指針訪問成員時的時間一般是一般 類訪問情況下的4倍左右,符合匯編語言輸出文件中的匯編語句的安排。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM