C++中的【菱形虛繼承】深入剖析

來源:互聯網
上載者:User

轉眼間有過了一個月了,自從【C/C++語言入門篇】連載結束後,已經很久沒有寫博了。最近一直忙著本科畢業論文和工作上的任務,加上一個對於我來說非常重要的事情進行中中。所以近段時間腦子一直處於繃緊狀態,發現自己的腦細胞還真是不夠用。加油!

 

 今天有朋友問到一個問題,那就是在C++的多重繼承中,出現菱形狀繼承的情況下,在構造對象時的記憶體分布及建構函式的調用流程上出現了問題。最後跟他解釋清楚之後,我感覺還是有必要把這個過程寫下來,有什麼說得不對的地方請大家提出寶貴意見,在此感謝,同時知道這裡面的朋友可以直接略過本篇。

 

好了,直接切入正題,所謂的菱形繼承,最簡單的構造如下:

 

class A

{

public:

    A( void ) : nVar( 0xaaaa0000 ){}

 

public:

    int nVar;

};

 

class B1 : public A

{

public:

    B1( void ){}

};

 

class B2 : public A

{

public:

    B2( void ){}

}; 

 

class C : public B1, public B2

{

public:

    C( void ){}

};

 

就是這樣一個多重繼承,用圖形化來表示之間的關係就是:

 

                           A

                         /   /

                        /     /

                      B1    B2

                       /      /

                        /    /

                          C

然後,在建立C的對象:

int main( void )

{

    C obj;

    return 0;

我想大家應該知道這樣將造成什麼情況,在這裡可以清楚的知道obj的大小為8,為什麼是8,先看記憶體分布:

假如obj的記憶體位址為0x0012ff18.

0x0012FF18:  00 00 aa aa 00 00 aa aa

 

看了obj對象的記憶體,裡面有2個A的副本,紅色的就是B1那條線繼承下來的記憶體,藍色就是B2那條線繼承下來的。因此A的建構函式被調用了兩次,這裡B1在前面,B2在後面是因為一對多繼承是從左至右分布記憶體的。

 

從這裡明顯知道這樣的結局肯定是很悲劇的。更可怕的是假如使用obj訪問nVar成員將導致編譯出錯:

obj.nVar = 0x100;

 

對nVar的訪問不明確,因為有兩個副本,編譯器不知道你到底要修改那個副本,從而導致編譯錯誤,這裡訪問成員函數也是一個道理。

 

那麼,有什麼解決辦法不讓這種現象出現呢,C++提出了虛繼承,以解決這個問題:

 

class A

{

public:

    A( void ) : nVar( 0xaaaa0000 ){}

 

public:

    int nVar;

};

 

 

class B1 : virtual public  A

 

{

public:

    B1( void ){}

};

 

 

class B2 : virtual public A

 

{

public:

    B2( void ){}

}; 

 

class C : public B1, public B2

{

public:

    C( void ){}

};

 

這樣繼承下來後,A就只會保留一個副本,再來看記憶體分布(這裡聲明,我使用的是VC2008版本來測試的):

假如obj的記憶體位址為:0x0012FF10

0x0012FF10:  0041580c 00415800 aaaa0000

 

可以清晰看出這裡0xaaaa0000隻有一個,而這時前面多了兩個值,obj的大小為12位元組,前面藍色的地址就是C類的虛基指標(vbtable)如果A有虛函數的話,在藍色和紅色之間還會加上虛函數表(vftable)這時就佔16位元組了。這裡就不具體介紹多重繼承的虛表的記憶體分布了。

 

好了,下面就是本文的重點了,來看看obj對象建立時,調用建構函式的流程:

流程大概就是:在obj建立時,首先會調用C類的建構函式,在建構函式中,首先會將兩個vbtable的位移賦值給前面的藍色部分記憶體。之後就會調用A的建構函式,調用之後再調B1和B2的建構函式。

 

用虛擬碼來表示:

C()

{

    vbtable;

    vbtable;

    A::A();

    B1::B1();

    B2::B2();

}

 

那麼在調用B1和B2的建構函式是時,按理說會調用A的建構函式,因為B1、B2也是繼承於A,但是為什麼沒有調用A的建構函式呢?來看看反組譯碼代碼:

 

首先看main函數:

    C obj;
004113DE  push        1   
004113E0  lea         ecx,[obj]
004113E3  call        C::C (4110E6h)

 

在紅色處調用C的建構函式,再來看C的建構函式:

00411460  push        ebp 
00411461  mov         ebp,esp
00411463  sub         esp,0CCh
00411469  push        ebx 
0041146A  push        esi 
0041146B  push        edi 
0041146C  push        ecx 
0041146D  lea         edi,[ebp-0CCh]
00411473  mov         ecx,33h
00411478  mov         eax,0CCCCCCCCh
0041147D  rep stos    dword ptr es:[edi]
0041147F  pop         ecx 
00411480  mov         dword ptr [ebp-8],ecx
00411483  cmp         dword ptr [ebp+8],0
00411487  je          C::C+47h (4114A7h)
00411489  mov         eax,dword ptr [this]
0041148C  mov         dword ptr [eax],offset C::`vbtable' (41580Ch)
00411492  mov         eax,dword ptr [this]
00411495  mov         dword ptr [eax+4],offset C::`vbtable' (415800h)
0041149C  mov         ecx,dword ptr [this]
0041149F  add         ecx,8
004114A2  call        A::A (4110EBh)
004114A7  push        0   
004114A9  mov         ecx,dword ptr [this]
004114AC  call          B2::B2 (4110AAh)
004114B1  push        0   
004114B3  mov         ecx,dword ptr [this]
004114B6  add         ecx,4
004114B9  call        B1::B1 (41107Dh)
004114BE  mov         eax,dword ptr [this]
004114C1  pop         edi 
004114C2  pop         esi 
004114C3  pop         ebx 
004114C4  add         esp,0CCh
004114CA  cmp         ebp,esp
004114CC  call        @ILT+330(__RTC_CheckEsp) (41114Fh)
004114D1  mov         esp,ebp
004114D3  pop         ebp 
004114D4  ret         4   

 

上面藍色的為加粗字型,可以看出在賦值vbtable。下面的紅色為加粗的部分就是調用A的建構函式。這不奇怪。

在調用A的構造之前有一句:add  ecx, 8 這一句的目的是為了將this定位到兩個vbtable之後,在調用A的建構函式時,直接往this所指向的記憶體位址下寫值:0xaaaa0000。因此就構成了布局:

0x0012FF10:     0041580c          00415800    aaaa0000

                        C::this/( vbtable)     vbtable         A::this

C的this在這裡看當然是0x0012ff10,A的this就是0x0012ff18,中間相隔兩個vbtable,其實this也就是某個類的起始地址,沒有什麼特別的。

 

到這裡,你可能注意到了藍色加粗和紅色加粗的兩條一樣的指令push 0,這條語句顯然是編譯器添加的,B2的建構函式明顯沒有參數,這樣push一個0進去有點類似隱含的一個參數,那麼push一個0進去到底做了些什麼呢,再看B1的建構函式:

 

00411550  push        ebp 
00411551  mov         ebp,esp
00411553  sub         esp,0CCh
00411559  push        ebx 
0041155A  push        esi 
0041155B  push        edi 
0041155C  push        ecx 
0041155D  lea          edi,[ebp-0CCh]
00411563  mov         ecx,33h
00411568  mov         eax,0CCCCCCCCh
0041156D  rep stos    dword ptr es:[edi]
0041156F  pop          ecx 
00411570  mov         dword ptr [ebp-8],ecx
00411573  cmp         dword ptr [ebp+8],0
00411577  je            B1::B1+3Dh (41158Dh)
00411579  mov         eax,dword ptr [this]
0041157C  mov         dword ptr [eax],offset B1::`vbtable' (415818h)
00411582  mov         ecx,dword ptr [this]
00411585  add         ecx,4
00411588  call          A::A (4110EBh)
0041158D  mov         eax,dword ptr [this]
00411590  pop         edi 
00411591  pop         esi 
00411592  pop         ebx 
00411593  add         esp,0CCh
00411599  cmp         ebp,esp
0041159B  call        @ILT+330(__RTC_CheckEsp) (41114Fh)
004115A0  mov         esp,ebp
004115A2  pop         ebp 
004115A3  ret         4 

 

紅色的那句指令很明顯,ebp+8正是函數的第一個參數,這裡雖然沒有,但是壓入了一個0,這樣一個cmp與0比較相等,執行藍色的跳轉直接躍過A的建構函式調用到綠色的那條指令。這樣便實現了只調用一次A的建構函式的功能。B2的建構函式也是同理,這裡就不介紹了。

 

有了這樣一個push 0 然後又檢查是否為零的操作,所以就算你在B1、B2中顯示調用A的建構函式,結果還是不會調用A的建構函式的。

形如: B1( void ): A(){} 因為判斷為零直接跳轉到建構函式的使用者代碼裡。

 

好了,本文就到這裡就差不多了,這裡只是介紹了虛繼承中建構函式調用的原理。望大家多多提意見哈。 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.