C++虛基類的實現機制:筆記 && C++學習筆記(10)——虛基類的作用

在《深度探索C++對象模型》裏,有一個問題,也是去公司面試的時候那些技術人員常問的問題:在C++中,obj是一個類的對象,p是指向obj的指針,該類裏面有個數據成員mem,請問obj.mem和p->mem在實現和效率上有什麼不同。


答案是:只有一種情況下才有重大差異,該情況必須滿足以下3個條件:

(1)、obj 是一個虛擬繼承的派生類的對象
(2)、mem是從虛擬基類派生下來的成員
(3)、p是基類類型的指針

當這種情況下,p->mem會比obj.mem多了兩個中間層。(也就是說在這種情況下,p->mem比obj.mem要明顯的慢,呵呵)

WHY?

如果好奇心比較重的話,請往下看 :)


1、虛基類的使用,和爲多態而實現的虛函數不同,是爲了解決多重繼承的二義性問題。

舉例如下:

class A
{
public:
    int a;
};

class B : virtual public A
{
public:
   int b;
};

class C :virtual public A
{
public:
   int c;
};

class D : public B, public C
{
public:
   int d;
};

上面這種菱形的繼承體系中,如果沒有virtual繼承,那麼D中就有兩個A的成員int a;繼承下來,使用的時候,就會有很多二義性。而加了virtual繼承,在D中就只有A的成員int a;的一份拷貝,該拷貝不是來自B,也不是來自C,而是一份單獨的拷貝,那麼,編譯器是怎麼實現的呢??

在回答這個問題之前,先想一下,sizeof(A),sizeof(B),sizeof(C),sizeof(D)是多少?(在32位x86的linux2.6下面,或者在vc2005下面)

在linux2.6下面,結果如下:sizeof(A) = 4; sizeof(B) = 12; sizeof(C) = 12; sizeof(D) = 24

sizeof(B)爲什麼是12呢,那是因爲多了一個指針(這一點和虛函數的實現一樣),那個指針是幹嘛的呢?

那麼sizeof(D)爲什麼是24呢?那是因爲除了繼承B中的b,C中的c,A中的a,和D自己的成員d之外,還繼承了B,C多出來的2個指針(B和C分別有一個)。再強調一遍,D中的int a不是來自B也不是來自C,而是另外的一份從A直接靠過來的成員。

如果聲明瞭D的對象d: D d;
那麼d的內存佈局如下:

vb_ptr: 繼承自B的指針
 
int b: 繼承自B公有成員
 
vc_ptr:繼承自C的指針
 
int c: 繼承自C的共有成員
 
int d: D自己的公有成員
 
int a: 繼承自A的公有成員
 


那麼以下的用法會發生什麼事呢?

D dD;
B *pb = &dD;
pb->a;

上面說過,dD中的int a不是繼承自B的,也不是繼承自C的,那麼這個B中的pb->a又會怎麼知道指向的是dD內存中的第六項呢?

那就是指針vb_ptr的妙用了。原理如下:(其實g++3.4.3的實現更加複雜,我不知道是出於什麼考慮,而我這裏只說原理,所以把過程和內容簡單化了)

首先,vb_ptr指向一個整數的地址,裏面放的整數是那個int a的距離dD開始處的位移(在這裏vb_ptr指向的地址裏面放的是20,以字節爲單位)。編譯器是這樣做的:

首先,找到vb_ptr(這個不用找,因爲在g++中,vb_ptr就是B*中的第一項,呵呵),然後取得vb_ptr指向的地址的內容(這個例子是20),最後把這個內容與指針pb相加,就得到pb->a的地址了。

所以說這種時候,用指針轉換多了兩個中間層才能找到基類的成員,而且是運行期間。

由此也可以推知dD中的vb_ptr和vc_ptr的內容都是一樣的,都是指向同一個地址,該地址就放20(在本例中)

如下的語句呢:

A *pa = &dD;
pa->a = 4;

這個語句不用轉換了,因爲編譯器在編譯期間就知道他把A中的成員插在dD中的那個地方了(在本例中是末尾),所以這個語句中的運行效率和dD.a是一樣的(至少也是差不多的)

這就是虛基類實現的基本原理。

注意的是:那些指針的位置和基類成員在派生類成員中的內存佈局是不確定的,也就是說標準裏面沒有規定int a必須要放在最後,只不過g++編譯器的實現而已。c++標準大概只規定了這套機制的原理,至於具體的實現,比如各成員的排放順序和優化,由各個編譯器廠商自己定~

 

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/jiangnanyouzi/archive/2009/01/06/3721091.aspx

 

 

 

 

---------------------------------------------------------------------------------------------------------------

虛基類的作用
    
當一個基類被聲明爲虛基類後,即使它成爲了多繼承鏈路上的公共基類,最後的派生類中也只有它的一個備份。例如:
class CBase { };
class CDerive1:virtual public CBase{ };
class CDerive2:virtual public CBase{ };
class CDerive12:public CDerive1,CDerive2{ };
則在類CDerive12的對象中,僅有類CBase的一個對象數據

虛基類的特點:

       虛基類構造函數的參數必須由最新派生出來的類負責初始化(即使不是直接繼承);
       虛基類的構造函數先於非虛基類的構造函數執行。
      
   
重寫“C++學習筆記(9)——使用範圍運算符解決繼承中的二義性問題 ”中的程序,觀察虛基類的作用
代碼如下:
      
/**//************************************************************************
* 混合繼承:多基類繼承與多重繼承
************************************************************************/
#include <IOSTREAM.H>
//基類
class CBase
...{
protected:
    int a;
public:
    CBase(int na)
    ...{
        a=na;
        cout<<"CBase constructor! ";
    }

    ~CBase()...{cout<<"CBase deconstructor! ";}
};

//派生類1(聲明CBase爲虛基類)
class CDerive1:virtual public CBase
...{
public:
    CDerive1(int na):CBase(na)
    ...{
        cout<<"CDerive1 constructor! ";
    }
   
    ~CDerive1()...{cout<<"CDerive1 deconstructor! ";}

    int GetA()...{return a;}
};

//派生類2(聲明CBase爲虛基類)
class CDerive2:virtual public CBase
...{
public:
    CDerive2(int na):CBase(na)
    ...{
        cout<<"CDerive2 constructor! ";
    }
    ~CDerive2()...{cout<<"CDerive2 deconstructor! ";}
    int GetA()...{return a;}
};

//子派生類
class CDerive12:public CDerive1,public CDerive2
...{
public:
    CDerive12(int na1,int na2,int na3):CDerive1(na1),CDerive2(na2),CBase(na3)
    ...{
        cout<<"CDerive12 constructor! ";
    }
    ~CDerive12()...{cout<<"CDerive12 deconstructor! ";}
};
void main()
...{
    CDerive12 obj(100,200,300);
    //得到從CDerive1繼承的值
    cout<<" from CDerive1 : a = "<<obj.CDerive1::GetA();
    //得到從CDerive2繼承的值
    cout<<" from CDerive2 : a = "<<obj.CDerive2::GetA()<<endl<<endl;
}
          
       
1. 子派生類對象的值:
      

             
     從上例可以看出,在類CDerived12的構造函數初始化表中,調用了間接基類CBase的構造函數,這對於非虛基類是非法的,但對於虛基類則是合法且必要的。
  對於派生類CDerived1和CDerived2,不論是其內部實現,還是實例化的對象,基類CBase是否是它們的虛基類是沒有影響的。受到影響的是它們的派生類CDerived12,因爲它從兩條路徑都能到達CBase。
        
2. 運行結果:
            

              
    由此可知,其公共基類的構造函數只調用了一次,並且優先於非基類的構造函數調用;並且發現,子派生類的對象obj的成員變量的值只有一個,所以,當公共基類CBase被聲明爲虛基類後,雖然它成爲CDerive1和CDerive2的公共基類,但子派生類CDerive12中也只有它的一個備份。可以仔細比較與例2的運行結果有什麼不同。 


本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/livelylittlefish/archive/2008/03/11/2171267.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章