C++ 虛繼承實現原理(虛基類表指針與虛基類表)


虛繼承和虛函數是完全無相關的兩個概念。

虛繼承是解決C++多重繼承問題的一種手段,從不同途徑繼承來的同一基類,會在子類中存在多份拷貝。這將存在兩個問題:其一,浪費存儲空間;第二,存在二義性問題,通常可以將派生類對象的地址賦值給基類對象,實現的具體方式是,將基類指針指向繼承類(繼承類有基類的拷貝)中的基類對象的地址,但是多重繼承可能存在一個基類的多份拷貝,這就出現了二義性。

虛繼承可以解決多種繼承前面提到的兩個問題:

虛繼承底層實現原理與編譯器相關,一般通過虛基類指針和虛基類表實現,每個虛繼承的子類都有一個虛基類指針(占用一個指針的存儲空間,4字節)和虛基類表(不占用類對象的存儲空間)(需要強調的是,虛基類依舊會在子類里面存在拷貝,只是僅僅最多存在一份而已,並不是不在子類里面了);當虛繼承的子類被當做父類繼承時,虛基類指針也會被繼承。 

實際上,vbptr指的是虛基類表指針(virtual base table pointer),該指針指向了一個虛基類表(virtual table),虛表中記錄了虛基類與本類的偏移地址;通過偏移地址,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持着公共基類(虛基類)的兩份同樣的拷貝,節省了存儲空間。

在這里我們可以對比虛函數的實現原理:他們有相似之處,都利用了虛指針(均占用類的存儲空間)和虛表(均不占用類的存儲空間)。

虛基類依舊存在繼承類中,只占用存儲空間;虛函數不占用存儲空間。

虛基類表存儲的是虛基類相對直接繼承類的偏移;而虛函數表存儲的是虛函數地址。

此篇博客有關於虛繼承詳細的內存分布情況

http://blog.csdn.net/xiejingfa/article/details/48028491

補充:

1、D繼承了B,C也就繼承了兩個虛基類指針

2、虛基類表存儲的是,虛基類相對直接繼承類的偏移(D並非是虛基類的直接繼承類,B,C才是)

  1.  
    #include<iostream>
  2.  
    using namespace std;
  3.  
     
  4.  
    class A //大小為4
  5.  
    {
  6.  
    public:
  7.  
    int a;
  8.  
    };
  9.  
    class B :virtual public A //大小為12,變量a,b共8字節,虛基類表指針4
  10.  
    {
  11.  
    public:
  12.  
    int b;
  13.  
    };
  14.  
    class C :virtual public A //與B一樣12
  15.  
    {
  16.  
    public:
  17.  
    int c;
  18.  
    };
  19.  
    class D :public B, public C //24,變量a,b,c,d共16,B的虛基類指針4,C的虛基類指針
  20.  
    {
  21.  
    public:
  22.  
    int d;
  23.  
    };
  24.  
     
  25.  
    int main()
  26.  
    {
  27.  
    A a;
  28.  
    B b;
  29.  
    C c;
  30.  
    D d;
  31.  
    cout << sizeof(a) << endl;
  32.  
    cout << sizeof(b) << endl;
  33.  
    cout << sizeof(c) << endl;
  34.  
    cout << sizeof(d) << endl;
  35.  
    system("pause");
  36.  
    return 0;
  37.  
    }

 

二: 從內存布局看C++虛繼承的實現原理

 

 

准備工作

1、VS2012使用命令行選項查看對象的內存布局

微軟的Visual Studio提供給用戶顯示C++對象在內存中的布局的選項:/d1reportSingleClassLayout。使用方法很簡單,直接在[工具(T)]選項下找到“Visual Studio命令提示(C)”后點擊即可。切換到cpp文件所在目錄下輸入如下的命令即可

      c1 [filename].cpp /d1reportSingleClassLayout[className]

其中[filename].cpp就是我們想要查看的class所在的cpp文件,[className]指我們想要查看的class的類名。(下面舉例說明...)

 

2、查看普通多繼承子類的內存布局

既然我們今天講的是虛基類和虛繼承,我們就先用上面介紹的命令提示工具查看一下普通多繼承子類的內存布局,可以跟后文虛繼承子類的內存布局情況加以比較。

 

我們新建一個名叫NormalInheritance的cpp文件,輸入一下內容。

  1.  
    /**
  2.  
    普通繼承(沒有使用虛基類)
  3.  
    */
  4.  
     
  5.  
    // 基類A
  6.  
    class A
  7.  
    {
  8.  
    public:
  9.  
    int dataA;
  10.  
    };
  11.  
     
  12.  
    class B : public A
  13.  
    {
  14.  
    public:
  15.  
    int dataB;
  16.  
    };
  17.  
     
  18.  
    class C : public A
  19.  
    {
  20.  
    public:
  21.  
    int dataC;
  22.  
    };
  23.  
     
  24.  
    class D : public B, public C
  25.  
    {
  26.  
    public:
  27.  
    int dataD;
  28.  
    };

上面是一個簡單的多繼承例子,我們啟動Visual Studio命令提示功能,切換到NormalInheritance.cpp文件所在目錄,輸入一下命令:

c1  NormalInheritance.cpp /d1reportSingleClassLayoutD

我們可以看到class D的內存布局如下:

 

 

從類D的內存布局可以看到A派生出B和C,B和C中分別包含A的成員。再由B和C派生出D,此時D包含了B和C的成員。這樣D中就總共出現了2個A成員。大家注意到左邊的幾個數字,這幾個數字表明了D中各成員在D中排列的起始地址,D中的五個成員變量(B::dataA、dataB、C::dataA、dataC、dataD)各占用4個字節,sizeof(D) = 20。

為了跟后文加以比較,我們再來看看B和C的內存布局:

 

                                  

 

虛繼承的內存分布情況

上面我們看到了普通多繼承子類的內存分布情況,下面我們進入主題,來看看典型的菱形虛繼承子類的內存分布情況。

我們新建一個名叫VirtualInheritance的cpp文件,輸入一下內容:

 

 

  1.  
    /**
  2.  
    虛繼承(虛基類)
  3.  
    */
  4.  
     
  5.  
    #include <iostream>
  6.  
     
  7.  
    // 基類A
  8.  
    class A
  9.  
    {
  10.  
    public:
  11.  
    int dataA;
  12.  
    };
  13.  
     
  14.  
    class B : virtual public A
  15.  
    {
  16.  
    public:
  17.  
    int dataB;
  18.  
    };
  19.  
     
  20.  
    class C : virtual public A
  21.  
    {
  22.  
    public:
  23.  
    int dataC;
  24.  
    };
  25.  
     
  26.  
    class D : public B, public C
  27.  
    {
  28.  
    public:
  29.  
    int dataD;
  30.  
    };

VirtualInheritance.cpp和NormalInheritance.cpp的不同點在與C和C繼承A時使用了virtual關鍵字,也就是虛繼承。同樣,我們看看B、C、D類的內存布局情況:

 

                                                                                                                                                                    

                                    

 

我們可以看到,菱形繼承體系中的子類在內存布局上和普通多繼承體系中的子類類有很大的不一樣。對於類B和C,sizeof的值變成了12,除了包含類A的成員變量dataA外還多了一個指針vbptr,類D除了繼承B、C各自的成員變量dataB、dataA和自己的成員變量外,還有兩個分別屬於B、C的指針。

那么類D對象的內存布局就變成如下的樣子:

 

vbptr:繼承自父類B中的指針

int dataB:繼承自父類B的成員變量

vbptr:繼承自父類C的指針

int dataC:繼承自父類C的成員變量

int dataD:D自己的成員變量

int A:繼承自父類A的成員變量

 

顯然,虛繼承之所以能夠實現在多重派生子類中只保存一份共有基類的拷貝,關鍵在於vbptr指針。那vbptr到底指的是什么?又是如何實現虛繼承的呢?其實上面的類D內存布局圖中已經給出答案:

                                                             

實際上,vbptr指的是虛基類表指針(virtual base table pointer),該指針指向了一個虛表(virtual table),虛表中記錄了vbptr與本類的偏移地址;第二項是vbptr到共有基類元素之間的偏移量。在這個例子中,類B中的vbptr指向了虛表D::$vbtable@B@,虛表表明公共基類A的成員變量dataA距離類B開始處的位移為20,這樣就找到了成員變量dataA,而虛繼承也不用像普通多繼承那樣維持着公共基類的兩份同樣的拷貝,節省了存儲空間。

 

為了進一步確定上面的想法是否正確,我們可以寫一個簡單的程序加以驗證:

  1.  
    int main()
  2.  
    {
  3.  
    D* d = new D;
  4.  
    d->dataA = 10;
  5.  
    d->dataB = 100;
  6.  
    d->dataC = 1000;
  7.  
    d->dataD = 10000;
  8.  
     
  9.  
    B* b = d; // 轉化為基類B
  10.  
    C* c = d; // 轉化為基類C
  11.  
    A* fromB = (B*) d;
  12.  
    A* fromC = (C*) d;
  13.  
     
  14.  
    std::cout << "d address : " << d << std::endl;
  15.  
    std::cout << "b address : " << b << std::endl;
  16.  
    std::cout << "c address : " << c << std::endl;
  17.  
    std::cout << "fromB address: " << fromB << std::endl;
  18.  
    std::cout << "fromC address: " << fromC << std::endl;
  19.  
    std::cout << std::endl;
  20.  
     
  21.  
    std::cout << "vbptr address: " << (int*)d << std::endl;
  22.  
    std::cout << " [0] => " << *(int*)(*(int*)d) << std::endl;
  23.  
    std::cout << " [1] => " << *(((int*)(*(int*)d)) + 1)<< std::endl; // 偏移量20
  24.  
    std::cout << "dataB value : " << *((int*)d + 1) << std::endl;
  25.  
    std::cout << "vbptr address: " << ((int*)d + 2) << std::endl;
  26.  
    std::cout << " [0] => " << *(int*)(*((int*)d + 2)) << std::endl;
  27.  
    std::cout << " [1] => " << *((int*)(*((int*)d + 2)) + 1) << std::endl; // 偏移量12
  28.  
    std::cout << "dataC value : " << *((int*)d + 3) << std::endl;
  29.  
    std::cout << "dataD value : " << *((int*)d + 4) << std::endl;
  30.  
    std::cout << "dataA value : " << *((int*)d + 5) << std::endl;
  31.  
    }

得到結果為:

 

                                             


免責聲明!

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



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