C++對象模型:單繼承,多繼承,虛繼承

什麼是對象模型

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

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

類中成員分類

數據成員分爲靜態和非靜態,成員函數有靜態非靜態以及虛函數ios

class data members:static和nonstatic函數

class data functions:static、nonstatic和virtual佈局

好比:spa

複製代碼

class Base
{
public:
 
    Base(int i) :baseI(i){};
  
    int getI(){ return baseI; }
 
    static void countI(){};
 
    virtual void print(void){ cout << "Base::print()"; }

    virtual ~Base(){}
 
private:
 
    int baseI;
 
    static int baseS;
};

複製代碼

對象模型分類

簡單對象模型:這個模型很是地簡單粗暴。在該模型下,對象由一系列的指針組成,每個指針都指向一個數據成員或成員函數,也便是說,每一個數據成員和成員函數在類中所佔的大小是相同的,都爲一個指針的大小。這樣有個好處——很容易算出對象的大小,不過賠上的是空間和執行期效率。因此這種對象模型並無被用於實際產品上。設計

表格驅動對象模型:把類中的數據分紅了兩個部分:數據部分與函數部分,並使用兩張表格,一張存放數據自己,一張存放函數的地址(也即函數比成員多一次尋址),而類對象僅僅含有兩個指針,分別指向上面這兩個表。這樣看來,對象的大小是固定爲兩個指針大小。這個模型也沒有用於實際應用於真正的C++編譯器上。指針

C++對象模型:正在使用的調試

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

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

單繼承(父類含虛函數)

原則:

對普通單繼承而言

  1. 子類與父類擁有各自的一個虛函數表
  2. 若子類並沒有overwrite父類虛函數,用父類虛函數
  3. 若子類重寫(overwrite)了父類的虛函數,則子類虛函數將覆蓋虛表中對應的父類虛函數
  4. 若子聲明瞭本身新的虛函數,則該虛函數地址將擴充到虛函數表最後

複製代碼

1 #include <iostream>
 2 using namespace std;
 3 
 4 class Base
 5 {
 6 public:
 7     virtual void fun1(){ cout << "Base fun1" << endl; }
 8     virtual void fun2(){ cout << "Base fun2" << endl; }
 9 private:
10     int a;
11 };
12 
13 class Derive :  public Base
14 {
15 public:
16     void fun2(){ cout << "Derive fun2" << endl; }
17     virtual void fun3(){}
18 private:
19     int b;
20 };
21 
22 int main()
23 {
24     Base b;
25     Derive d;
26     Base *p = &d;
27     p->fun1();
28     p->fun2();
29 
30     system("pause");
31     return 0;
32 }

複製代碼

輸出:

 

調試:

 

對象模型:

事實上vs調試並不能看到完整信息(好比virtual fun3以及以後提到到虛基類指針),正確的應該是

通常多繼承

這裏講的是不考慮菱形繼承的多繼承,由於菱形繼承須要用到虛繼承,放到以後考慮

原則:

  1. 若子類新增虛函數,放在聲明的第一個父類的虛函數表中
  2. 若子類重寫了父類的虛函數,全部父類的虛函數表都要改變:如fun1
  3. 內存佈局中,父類按照其聲明順序排列

複製代碼

1 #include <iostream>
 2 using namespace std;
 3 
 4 class Base1
 5 {
 6 public:
 7     virtual void fun1(){}
 8 private:
 9     int m_base1;
10 };
11 
12 class Base2
13 {
14 public:
15     virtual void fun1(){}
16     virtual void fun2(){}
17 private:
18     int m_base2;
19 };
20 
21 class Derive :  public Base1,public Base2
22 {
23 public:
24     void fun1(){}
25     virtual void fun3(){}
26 private:
27     int m_derive;
28 };
29 
30 int main()
31 {
32     Base1 b1;
33     Base2 b2;
34     Derive d;
35 
36     cout <<"b1:" <<sizeof(b1) << endl;
37     cout << "b2:" << sizeof(b2) << endl;
38     cout <<"d:" << sizeof(d) << endl;
39     system("pause");
40     return 0;
41 }

複製代碼

輸出:

各個類對象的大小

調試:注意觀察fun1

對象模型:

簡單虛繼承

原則:

虛繼承解決了菱形繼承中最派生類擁有多個間接父類實例的狀況

  1. 虛繼承的子類,若是自己定義了新的虛函數,則編譯器爲其生成一個新的虛函數指針(vptr)以及一張虛函數表。該vptr位於對象內存最前面(對比非虛繼承:直接擴展父類虛函數表)
  2. 虛繼承的子類也單獨保留了父類的vprt與虛函數表
  3. 虛繼承的子類有虛基類表指針(vbptr)

在C++對象模型中,虛繼承而來的子類會生成一個隱藏的虛基類指針(vbptr),在Microsoft Visual C++中,虛基類表指針老是在虛函數表指針以後,於是,對某個類實例來講,若是它有虛基類指針,那麼虛基類指針可能在實例的0字節偏移處(該類沒有vptr時,vbptr就處於類實例內存佈局的最前面,不然vptr處於類實例內存佈局的最前面),也可能在類實例的4字節偏移處。

 

虛基類表也由多個條目組成,條目中存放的是偏移值。

第一個條目存放虛基類表指針(vbptr)所在地址到該類內存首地址的偏移值

第2、第三...個條目依次爲該類的最左虛繼承父類、次左虛繼承父類...的內存地址相對於虛基類表指針的偏移值。

複製代碼

1 #include <iostream>
 2 using namespace std;
 3 
 4 class Base
 5 {
 6 public:
 7     virtual void fun1(){}
 8     virtual void fun2(){}
 9 private:
10     int m_base;
11 };
12 
13 class Derive : virtual public Base
14 {
15 public:
16     void fun1(){}
17     virtual void fun3(){}
18 private:
19     int m_derive;
20 };
21 
22 int main()
23 {
24     Base b;
25     Derive d;
26 
27     system("pause");
28     return 0;
29 }

複製代碼

對象模型:

 

菱形虛繼承

菱形虛繼承是多繼承和虛繼承的複合,直接畫一個對象模型吧:

筆記原圖:

參考:圖說C++對象模型:對象內存佈局詳解

相關文章
相關標籤/搜索