【c++工程實踐】內存模型

0.文章內容簡介

這篇文章主要來討論C++對象在內存中的佈局,屬於第二個概念的研究範疇。而C++直接支持面向對象程序設計部分則很少講。文章主要內容以下:前端

  • 虛函數表解析。含有虛函數或其父類含有虛函數的類,編譯器都會爲其添加一個虛函數表,vptr,先了解虛函數表的構成,有助對C++對象模型的理解。
  • 虛基類表解析。虛繼承產生虛基類表(vbptr),虛基類表的內容與虛函數表徹底不一樣,咱們將在講解虛繼承時介紹虛函數表。
  • 對象模型概述:介紹簡單對象模型、表格驅動對象模型,以及非繼承狀況下的C++對象模型。
  • 繼承下的C++對象模型。分析C++類對象在下面情形中的內存佈局:
    1. 單繼承:子類單一繼承自父類,分析了子類重寫父類虛函數、子類定義了新的虛函數狀況下子類對象內存佈局。
    2. 多繼承:子類繼承於多個父類,分析了子類重寫父類虛函數、子類定義了新的虛函數狀況下子類對象內存佈局,同時分析了非虛繼承下的菱形繼承。
    3. 虛繼承:分析了單一繼承下的虛繼承、多重基層下的虛繼承、重複繼承下的虛繼承。
  • 理解對象的內存佈局以後,咱們能夠分析一些問題:
    1. C++封裝帶來的佈局成本是多大?
    2. 由空類組成的繼承層次中,每一個類對象的大小是多大?

至於其餘與內存有關的知識,我假設你們都有必定的瞭解,如內存對齊,指針操做等。本文初看可能晦澀難懂,要求讀者有必定的C++基礎,對概念一有必定的掌握。ios

1.何爲C++對象模型?

引用《深度探索C++對象模型》這本書中的話:算法

有兩個概念能夠解釋C++對象模型:函數

  1. 語言中直接支持面向對象程序設計的部分。
  2. 對於各類支持的底層實現機制。

直接支持面向對象程序設計,包括了構造函數、析構函數、多態、虛函數等等,這些內容在不少書籍上都有討論,也是C++最被人熟知的地方(特性)。而對象模型的底層實現機制倒是不多有書籍討論的。對象模型的底層實現機制並未標準化,不一樣的編譯器有必定的自由來設計對象模型的實現細節。對象模型研究的是對象在存儲上的空間與時間上的更優,並對C++面向對象技術加以支持,如以虛指針、虛表機制支持多態特性。佈局

舉個例子,下面一段代碼:this

1 class A {
2  public:
3   void foo(){ cout << "A foo" << endl; }
4 };

oo將編譯成:編碼

1 void foo(const A* this)( cout << "A foo" << endl; }

調用a.foo(),編譯器將轉換成foo(&a)spa

有趣的是,A* pa = NULL; pa->foo();也沒有異常退出,由於沒有經過this引用任何成員變量,這個時候不過this指針爲NULL而已。翻譯

靜態成員函數

上面說的只是面向對象的非靜態成員函數,若是說到類裏面的靜態成員函數,解釋又是另一個,請看下文。設計

一、靜態數據成員   
  特色:   
    A、內存分配:在程序的全局數據區分配。   
    B、初始化和定義:   
     a、靜態數據成員定義時要分配空間,因此不能在類聲明中定義。   
     b、爲了不在多個使用該類的源文件中,對其重複定義,所在,不能在類的頭文件中   
      定義。     
     c、靜態數據成員由於程序一開始運行就必需存在,因此其初始化的最佳位置在類的內部實現。   
    C、特色   
     a、對相於   public,protected,private   關鍵字的影響它和普通數據成員同樣,     
     b、由於其空間在全局數據區分配,屬於全部本類的對象共享,因此,它不屬於特定的類對象,在沒產生類對象時其做用域就可見,即在沒有產生類的實例時,咱們就能夠操做它。  
    D、訪問形式   
     a、   類對象名.靜態數據成員名   
     b、   類類型名::   靜態數據成員名   
    E、靜態數據成員,主要用在類的全部實例都擁有的屬性上。好比,對於一個存款類,賬號相對   於每一個實例都是不一樣的,但每一個實例的利息是相同的。因此,應該把利息設爲存款類的靜態數據成員。這有兩個好處,第一,無論定義多少個存款類對象,利息數據成員都共享分配在全局區的內存,因此節省存貯空間。第二,一旦利息須要改變時,只要改變一次,則全部存款類對象的利息全改變過來了,由於它們其實是共用一個東西。   
    
  二、靜態成員函數   
  特色:   
    A、靜態成員函數與類相聯繫,不與類的對象相聯繫。   
    B、靜態成員函數不能訪問非靜態數據成員。緣由很簡單,非靜態數據成員屬於特定的類實例。   
  做用:   
    主要用於對靜態數據成員的操做。   
  調用形式:   
    A、類對象名.靜態成員函數名()   
    B、類類型名::   靜態成員函數名()

 1 #include <iostream>
 2 #include <typeinfo>
 3 using namespace std;
 4 
 5 class A {
 6  public:
 7   static int count;
 8   void foo(){ cout << "A foo this=" << this << endl; }    // 若是這樣聲明和定義一個成員函數,將直接產生一個 foo(A& this) 類型的函數
 9   static void goo(){ cout <<"static A goo"<< endl; }    // 靜態函數沒有 this 指針
10   void too(){ cout << typeid(*this).name() << endl; }
11 };
12 
13 int A::count = 0;
14 
15 class B : public A {
16  public:
17     // 若是靜態函數只能經過域運算符來調用的話,那class在靜態意義下就成了命名域的概念了
18     // 若是沒有下面這個函數,A類的goo函數將會繼承下來,說明做爲類的命名空間,也能夠繼承
19   static void goo(){ cout << "static B goo" << endl;}        // 一個問題,靜態的成員函數,是怎麼區分開的呢?
20 };
21 
22 int main(void) {
23   A a;
24   a.goo();        // 是不是直接翻譯成 A::goo() 
25   a.foo();        // this 是關鍵字,不能拿來做爲一個全局函數的參數,在轉成 foo(&a) 的時候,必定是調用 foo(A& this) 這個函數
26   a.too();
27   cout << "============" << endl;
28   A* pa = nullptr;
29   pa->goo();        // 靜態也跟普通函數同樣,沒有多態效果
30   pa->foo();        // 靜態也跟普通函數同樣,沒有多態效果
31   cout << "============" << endl;
32   B::goo();        // 這個 foo 沒有帶參數,只能調用靜態的,靜態的就直接編譯成相似全局函數的不帶 this 參數的類型
33   A::goo();        // 這樣調用是正確的,這說明它沒有 this 指針做爲形參
34   B::A::goo();
35 
36   return 0;
37 }

第27行a.goo()經過對象調用靜態函數,已經經過類型識別,被編譯器替換成A::goo(),這個是由編譯器作的,因此替換以後a就只定義了,可是沒用引用過。換句話,static的成員函數,只能經過域運算符來調用,不管你是用對象調用仍是用指針調用。static的成員變量,也是如此,只能經過翻譯成域運算符來調用。

第34行,說明了「類其實除了能夠定義變量,還有一個重要的做用就是它是個命名域,至關於std::cout這樣。並且,這個命名域,還能繼承下來。」

C++和C語言的編譯方式不一樣。C語言中的函數在編譯時名字不變,或者只是簡單的加一個下劃線_(不一樣的編譯器有不一樣的實現),例如,func() 編譯後爲 func() 或 _func()。而C++中的函數在編譯時會根據命名空間、類、參數簽名等信息進行從新命名,造成新的函數名。這個重命名的過程是經過一個特殊的算法來實現的,稱爲名字編碼(Name Mangling)Name Mangling 是一種可逆的算法,既能夠經過現有函數名計算出新函數名,也能夠經過新函數名逆向推演出原有函數名。Name Mangling 能夠確保新函數名的惟一性,只要命名空間、所屬的類、參數簽名等有一個不一樣,那麼產生的新函數名也不一樣。

3.理解虛函數表

3.1.多態與虛表

C++中虛函數的做用主要是爲了實現多態機制。多態,簡單來講,是指在繼承層次中,父類的指針能夠具備多種形態——當它指向某個子類對象時,經過它可以調用到子類的函數,而非父類的函數。

1 class Base {     virtual void print(void);    }
2 class Drive1 :public Base{    virtual void print(void);    }
3 class Drive2 :public Base{    virtual void print(void);    }
4 Base * ptr1 = new Base; 
5 Base * ptr2 = new Drive1;  
6 Base * ptr3 = new Drive2;
7 ptr1->print(); //調用Base::print()
8 prt2->print();//調用Drive1::print()
9 prt3->print();//調用Drive2::print()

這是一種運行期多態,即父類指針惟有在程序運行時才能知道所指的真正類型是什麼。這種運行期決議,是經過虛函數表來實現的。

3.2.使用指針訪問虛表

若是咱們豐富咱們的Base類,使其擁有多個virtual函數:

1 class Base {
2  public:
3   Base(int i) :baseI(i){};
4   virtual void print(void){ cout << "調用了虛函數Base::print()"; }
5   virtual void setI(){cout<<"調用了虛函數Base::setI()";}
6   virtual ~Base(){}
7  private:
8   int baseI;
9 };

當一個類自己定義了虛函數,或其父類有虛函數時,爲了支持多態機制,編譯器將爲該類添加一個虛函數指針(vptr)。虛函數指針通常都放在對象內存佈局的第一個位置上,這是爲了保證在多層繼承或多重繼承的狀況下能以最高效率取到虛函數表。

當vprt位於對象內存最前面時,對象的地址即爲虛函數指針地址。咱們能夠取得虛函數指針的地址:

  Base b(1000);
  int * vptrAdree = (int *)(&b);  
  cout << "vptr=" << vptrAdree << ", baseI_addr=" << (int* )&(b.baseI) << endl;

運行代碼出結果:

咱們強行把類對象的地址轉換爲 int* 類型,取得了虛函數指針的地址。能夠看到,虛表指針和成員函數是緊挨着的。

虛函數指針指向虛函數表,虛函數表中存儲的是一系列虛函數的地址,虛函數地址出現的順序與類中虛函數聲明的順序一致。對虛函數指針地址值,能夠獲得虛函數表的地址,也便是虛函數表第一個虛函數的地址:

1     typedef void(*Fun)(void);
2     Fun vfunc = (Fun)*( (int *)*(int*)(&b));
3     cout << "第一個虛函數的地址是:" << (int *)*(int*)(&b) << endl;
4     cout << "經過地址,調用虛函數Base::print():";
5     vfunc(); 
  • 咱們把虛表指針的值取出來: *(int*)(&b),它是一個地址,虛函數表的地址
  • 把虛函數表的地址強制轉換成 int* : ( int *) *( int* )( &b )
  • 再把它轉化成咱們Fun指針類型 : (Fun )*(int *)*(int*)(&b)

這樣,咱們就取得了類中的第一個虛函數,咱們能夠經過函數指針訪問它。
運行結果:

同理,第二個虛函數setI()的地址爲:

(int * )(*(int*)(&b)+1)

一樣能夠經過函數指針訪問它。

到目前爲止,咱們知道了類中虛表指針vprt的由來,知道了虛函數表中的內容,以及如何經過指針訪問虛函數表。

4.對象模型概述

在C++中,有兩種數據成員(class data members):static 和nonstatic,以及三種類成員函數(class member functions):static、nonstatic和virtual:

 1 class Base
 2 {
 3 public:
 4  
 5     Base(int i) :baseI(i){};
 6   
 7     int getI(){ return baseI; }
 8  
 9     static void countI(){};
10  
11     virtual ~Base(){}
12 
13     virtual void print(void){ cout << "Base::print()"; }
14  
15 private:
16  
17     int baseI;
18  
19     static int baseS;
20 };

如今咱們有一個類Base,它包含了上面這5中類型的數據或函數:

那麼,這個類在內存中將被如何表示?5種數據都是連續存放的嗎?如何佈局才能支持C++多態? 咱們的C++標準與編譯器將如何塑造出各類數據成員與成員函數呢?

4.1.非繼承下的C++對象模型

概述:在此模型下,nonstatic 數據成員被置於每個類對象中,而static數據成員被置於類對象以外。static與nonstatic函數也都放在類對象以外,而對於virtual 函數,則經過虛函數表+虛指針來支持,具體以下:

  • 每一個類生成一個表格,稱爲虛表(virtual table,簡稱vtbl)。虛表中存放着一堆指針,這些指針指向該類每個虛函數。虛表中的函數地址將按聲明時的順序排列,不過當子類有多個重載函數時例外,後面會討論。
  • 每一個類對象都擁有一個虛表指針(vptr),由編譯器爲其生成。虛表指針的設定與重置皆由類的複製控制(也便是構造函數、析構函數、賦值操做符)來完成。vptr的位置爲編譯器決定,傳統上它被放在全部顯示聲明的成員以後,不過如今許多編譯器把vptr放在一個類對象的最前端。關於數據成員佈局的內容,在後面會詳細分析。
    另外,虛函數表的前面設置了一個指向type_info的指針,用以支持RTTI(Run Time Type Identification,運行時類型識別)。RTTI是爲多態而生成的信息,包括對象繼承關係,對象自己的描述等,只有具備虛函數的對象在會生成。

在此模型下,Base的對象模型如圖:

先在VS上驗證類對象的佈局:

Base b(1000);

可見對象b含有一個vfptr,即vprt。而且只有nonstatic數據成員被放置於對象內。咱們展開vfprt:

vfptr中有兩個指針類型的數據(地址),第一個指向了Base類的析構函數,第二個指向了Base的虛函數print,順序與聲明順序相同。
這與上述的C++對象模型相符合。也能夠經過代碼來進行驗證:

 1 void testBase( Base&p)
 2 {
 3     cout << "對象的內存起始地址:" << &p << endl;
 4     cout << "type_info信息:" << endl;
 5     RTTICompleteObjectLocator str = *((RTTICompleteObjectLocator*)*((int*)*(int*)(&p) - 1));
 6  
 7  
 8     string classname(str.pTypeDescriptor->name);
 9     classname = classname.substr(4, classname.find("@@") - 4);
10     cout <<  "根據type_info信息輸出類名:"<< classname << endl;
11  
12     cout << "虛函數表地址:" << (int *)(&p) << endl;
13  
14     //驗證虛表
15     cout << "虛函數表第一個函數的地址:" << (int *)*((int*)(&p)) << endl;
16     cout << "析構函數的地址:" << (int* )*(int *)*((int*)(&p)) << endl;
17     cout << "虛函數表中,第二個虛函數即print()的地址:" << ((int*)*(int*)(&p) + 1) << endl;
18  
19     //經過地址調用虛函數print()
20     typedef void(*Fun)(void);
21     Fun IsPrint=(Fun)* ((int*)*(int*)(&p) + 1);
22     cout << endl;
23     cout<<"調用了虛函數"24     IsPrint(); //若地址正確,則調用了Base類的虛函數print()
25     cout << endl;
26  
27     //輸入static函數的地址
28     p.countI();//先調用函數以產生一個實例
29     cout << "static函數countI()的地址:" << p.countI << endl;
30  
31     //驗證nonstatic數據成員
32     cout << "推測nonstatic數據成員baseI的地址:" << (int *)(&p) + 1 << endl;
33     cout << "根據推測出的地址,輸出該地址的值:" << *((int *)(&p) + 1) << endl;
34     cout << "Base::getI():" << p.getI() << endl;
35  
36 }
37 Base b(1000);
38 testBase(b);

結果分析:

  • 經過 (int *)(&p)取得虛函數表的地址
  • type_info信息的確存在於虛表的前一個位置。經過((int)(int*)(&p) - 1))取得type_infn信息,併成功得到類的名稱的Base
  • 虛函數表的第一個函數是析構函數。
  • 虛函數表的第二個函數是虛函數print(),取得地址後經過地址調用它(而非經過對象),驗證正確
  • 虛表指針的下一個位置爲nonstatic數據成員baseI。
  • 能夠看到,static成員函數的地址段位與虛表指針、baseI的地址段位不一樣。

好的,至此咱們瞭解了非繼承下類對象五種數據在內存上的佈局,也知道了在每個虛函數表前都有一個指針指向type_info,負責對RTTI的支持。而加入繼承後類對象在內存中該如何表示呢?

 

5.繼承下的C++對象模型

5.1.單繼承

若是咱們定義了派生類

 1 class Derive : public Base
 2 {
 3 public:
 4     Derive(int d) :Base(1000),      DeriveI(d){};
 5     //overwrite父類虛函數
 6     virtual void print(void){ cout << "Drive::Drive_print()" ; }
 7     // Derive聲明的新的虛函數
 8         virtual void Drive_print(){ cout << "Drive::Drive_print()" ; }
 9     virtual ~Derive(){}
10 private:
11     int DeriveI;
12 };

繼承類圖爲:

一個派生類如何在機器層面上塑造其父類的實例呢?

在C++對象模型中,對於通常繼承(這個通常是相對於虛擬繼承而言),若子類重寫(overwrite)了父類的虛函數,則子類虛函數將覆蓋虛表中對應的父類虛函數(注意子類與父類擁有各自的一個虛函數表);若子類並沒有overwrite父類虛函數,而是聲明瞭本身新的虛函數,則該虛函數地址將擴充到虛函數表最後(在vs中沒法經過監視看到擴充的結果,不過咱們經過取地址的方法能夠作到,子類新的虛函數確實在父類子物體的虛函數表末端)。而對於虛繼承,若子類overwrite父類虛函數,一樣地將覆蓋父類子物體中的虛函數表對應位置,而若子類聲明瞭本身新的虛函數,則編譯器將爲子類增長一個新的虛表指針vptr,這與通常繼承不一樣,在後面再討論。

咱們使用代碼來驗證以上模型

 1 typedef void(*Fun)(void);
 2  
 3 int main()
 4 {
 5     Derive d(2000);
 6     //[0]
 7     cout << "[0]Base::vptr";
 8     cout << "\t地址:" << (int *)(&d) << endl;
 9         //vprt[0]
10         cout << "  [0]";
11         Fun fun1 = (Fun)*((int *)*((int *)(&d)));
12         fun1();
13         cout << "\t地址:\t" << *((int *)*((int *)(&d))) << endl;
14  
15         //vprt[1]析構函數沒法經過地址調用,故手動輸出
16         cout << "  [1]" << "Derive::~Derive" << endl;
17  
18         //vprt[2]
19         cout << "  [2]";
20         Fun fun2 = (Fun)*((int *)*((int *)(&d)) + 2);
21         fun2();
22         cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 2) << endl;
23     //[1]
24     cout << "[2]Base::baseI=" << *(int*)((int *)(&d) + 1);
25     cout << "\t地址:" << (int *)(&d) + 1;
26     cout << endl;
27     //[2]
28     cout << "[2]Derive::DeriveI=" << *(int*)((int *)(&d) + 2);
29     cout << "\t地址:" << (int *)(&d) + 2;
30     cout << endl;
31     getchar();
32 }

運行結果:

這個結果與咱們的對象模型符合。

5.2.多繼承

5.2.1通常的多重繼承(非菱形繼承)

單繼承中(通常繼承),子類會擴展父類的虛函數表。在多繼承中,子類含有多個父類的子對象,該往哪一個父類的虛函數表擴展呢?當子類overwrite了父類的函數,須要覆蓋多個父類的虛函數表嗎?

  • 子類的虛函數被放在聲明的第一個基類的虛函數表中
  • overwrite時,全部基類的print()函數都被子類的print()函數覆蓋。
  • 內存佈局中,父類按照其聲明順序排列。

其中第二點保證了父類指針指向子類對象時,老是可以調用到真正的函數。

爲了方便查看,咱們把代碼都粘貼過來

 1 class Base
 2 {
 3 public:
 4  
 5     Base(int i) :baseI(i){};
 6     virtual ~Base(){}
 7  
 8     int getI(){ return baseI; }
 9  
10     static void countI(){};
11  
12     virtual void print(void){ cout << "Base::print()"; }
13  
14 private:
15  
16     int baseI;
17  
18     static int baseS;
19 };
20 class Base_2
21 {
22 public:
23     Base_2(int i) :base2I(i){};
24 
25     virtual ~Base_2(){}
26 
27     int getI(){ return base2I; }
28 
29     static void countI(){};
30 
31     virtual void print(void){ cout << "Base_2::print()"; }
32  
33 private:
34  
35     int base2I;
36  
37     static int base2S;
38 };
39  
40 class Drive_multyBase :public Base, public Base_2
41 {
42 public:
43 
44     Drive_multyBase(int d) :Base(1000), Base_2(2000) ,Drive_multyBaseI(d){};
45  
46     virtual void print(void){ cout << "Drive_multyBase::print" ; }
47  
48     virtual void Drive_print(){ cout << "Drive_multyBase::Drive_print" ; }
49  
50 private:
51     int Drive_multyBaseI;
52 };

繼承類圖爲:

此時Drive_multyBase 的對象模型是這樣的:

咱們使用代碼驗證:

 1 typedef void(*Fun)(void);
 2  
 3 int main()
 4 {
 5     Drive_multyBase d(3000);
 6     //[0]
 7     cout << "[0]Base::vptr";
 8     cout << "\t地址:" << (int *)(&d) << endl;
 9  
10         //vprt[0]析構函數沒法經過地址調用,故手動輸出
11         cout << "  [0]" << "Derive::~Derive" << endl;
12  
13         //vprt[1]
14         cout << "  [1]";
15         Fun fun1 = (Fun)*((int *)*((int *)(&d))+1);
16         fun1();
17         cout << "\t地址:\t" << *((int *)*((int *)(&d))+1) << endl;
18  
19  
20         //vprt[2]
21         cout << "  [2]";
22         Fun fun2 = (Fun)*((int *)*((int *)(&d)) + 2);
23         fun2();
24         cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 2) << endl;
25  
26  
27     //[1]
28     cout << "[1]Base::baseI=" << *(int*)((int *)(&d) + 1);
29     cout << "\t地址:" << (int *)(&d) + 1;
30     cout << endl;
31  
32  
33     //[2]
34     cout << "[2]Base_::vptr";
35     cout << "\t地址:" << (int *)(&d)+2 << endl;
36  
37         //vprt[0]析構函數沒法經過地址調用,故手動輸出
38         cout << "  [0]" << "Drive_multyBase::~Derive" << endl;
39  
40         //vprt[1]
41         cout << "  [1]";
42         Fun fun4 = (Fun)*((int *)*((int *)(&d))+1);
43         fun4();
44         cout << "\t地址:\t" << *((int *)*((int *)(&d))+1) << endl;
45  
46     //[3]
47     cout << "[3]Base_2::base2I=" << *(int*)((int *)(&d) + 3);
48     cout << "\t地址:" << (int *)(&d) + 3;
49     cout << endl;
50  
51     //[4]
52     cout << "[4]Drive_multyBase::Drive_multyBaseI=" << *(int*)((int *)(&d) + 4);
53     cout << "\t地址:" << (int *)(&d) + 4;
54     cout << endl;
55  
56     getchar();
57 }

運行結果:

5.2.2 菱形繼承

菱形繼承也稱爲鑽石型繼承或重複繼承,它指的是基類被某個派生類簡單重複繼承了屢次。這樣,派生類對象中擁有多份基類實例(這會帶來一些問題)。爲了方便敘述,咱們不使用上面的代碼了,而從新寫一個重複繼承的繼承層次:

 1 class B
 2  
 3 {
 4  
 5 public:
 6  
 7     int ib;
 8  
 9 public:
10  
11     B(int i=1) :ib(i){}
12  
13     virtual void f() { cout << "B::f()" << endl; }
14  
15     virtual void Bf() { cout << "B::Bf()" << endl; }
16  
17 };
18  
19 class B1 : public B
20  
21 {
22  
23 public:
24  
25     int ib1;
26  
27 public:
28  
29     B1(int i = 100 ) :ib1(i) {}
30  
31     virtual void f() { cout << "B1::f()" << endl; }
32  
33     virtual void f1() { cout << "B1::f1()" << endl; }
34  
35     virtual void Bf1() { cout << "B1::Bf1()" << endl; }
36  
37  
38  
39 };
40  
41 class B2 : public B
42  
43 {
44  
45 public:
46  
47     int ib2;
48  
49 public:
50  
51     B2(int i = 1000) :ib2(i) {}
52  
53     virtual void f() { cout << "B2::f()" << endl; }
54  
55     virtual void f2() { cout << "B2::f2()" << endl; }
56  
57     virtual void Bf2() { cout << "B2::Bf2()" << endl; }
58  
59 };
60  
61  
62 class D : public B1, public B2
63  
64 {
65  
66 public:
67  
68     int id;
69  
70  
71  
72 public:
73  
74     D(int i= 10000) :id(i){}
75  
76     virtual void f() { cout << "D::f()" << endl; }
77  
78     virtual void f1() { cout << "D::f1()" << endl; }
79  
80     virtual void f2() { cout << "D::f2()" << endl; }
81  
82     virtual void Df() { cout << "D::Df()" << endl; }
83  
84 };

這時,根據單繼承,咱們能夠分析出B1,B2類繼承於B類時的內存佈局。又根據通常多繼承,咱們能夠分析出D類的內存佈局。咱們能夠得出D類子對象的內存佈局以下圖:

D類對象內存佈局中,圖中青色表示b1類子對象實例,黃色表示b2類子對象實例,灰色表示D類子對象實例。從圖中能夠看到,因爲D類間接繼承了B類兩次,致使D類對象中含有兩個B類的數據成員ib,一個屬於來源B1類,一個來源B2類。這樣不只增大了空間,更重要的是引發了程序歧義:

D d;
 
d.ib =1 ; //二義性錯誤,調用的是B1的ib仍是B2的ib? d.B1::ib = 1; //正確 d.B2::ib = 1; //正確

儘管咱們能夠經過明確指明調用路徑以消除二義性,但二義性的潛在性尚未消除,咱們能夠經過虛繼承來使D類只擁有一個ib實體。

相關文章
相關標籤/搜索