C++物件模型:單繼承,多繼承,虛繼承
什麼是物件模型
有兩個概念可以解釋C++物件模型:
語言中直接支援面向物件程式設計的部分。
對於各種支援的底層實現機制。
類中成員分類
資料成員分為靜態和非靜態,成員函式有靜態非靜態以及虛擬函式
class data members:static和nonstatic
class data functions:static、nonstatic和virtual
比如:
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 函式,則通過虛擬函式表+虛指標來支援:
- 每個類生成一個表格,稱為虛表(virtual table,簡稱vtbl)。虛表中存放著一堆指標,這些指標指向該類每一個虛擬函式。虛表中的函式地址將按宣告時的順序排列
- 每個類物件都擁有一個虛表指標(vptr),由編譯器為其生成。虛表指標的設定與重置皆由類的複製控制(也即是建構函式、解構函式、賦值操作符)來完成。vptr的位置為編譯器決定,傳統上它被放在所有顯示宣告的成員之後,不過現在許多編譯器把vptr放在一個類物件的最前端(也就是說物件的地址就是vptr的地址)
- 虛擬函式表的前面設定了一個指向type_info的指標,用以支援RTTI(Run Time Type Identification,執行時型別識別)。RTTI是為多型而生成的資訊,包括物件繼承關係,物件本身的描述等,只有具有虛擬函式的物件在會生成。
單繼承(父類含虛擬函式)
原則:
對普通單繼承而言
- 子類與父類擁有各自的一個虛擬函式表
- 若子類並無overwrite父類虛擬函式,用父類虛擬函式
- 若子類重寫(overwrite)了父類的虛擬函式,則子類虛擬函式將覆蓋虛表中對應的父類虛擬函式
- 若子聲明瞭自己新的虛擬函式,則該虛擬函式地址將擴充到虛擬函式表最後
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以及之後提到到虛基類指標),正確的應該是
一般多繼承
這裡講的是不考慮菱形繼承的多繼承,因為菱形繼承需要用到虛繼承,放到之後考慮
原則:
- 若子類新增虛擬函式,放在宣告的第一個父類的虛擬函式表中
- 若子類重寫了父類的虛擬函式,所有父類的虛擬函式表都要改變:如fun1
- 記憶體佈局中,父類按照其宣告順序排列
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
物件模型:
簡單虛繼承
原則:
虛繼承解決了菱形繼承中最派生類擁有多個間接父類例項的情況
- 虛繼承的子類,如果本身定義了新的虛擬函式,則編譯器為其生成一個新的虛擬函式指標(vptr)以及一張虛擬函式表。該vptr位於物件記憶體最前面(對比非虛繼承:直接擴充套件父類虛擬函式表)
- 虛繼承的子類也單獨保留了父類的vprt與虛擬函式表
- 虛繼承的子類有虛基類表指標(vbptr)
在C++物件模型中,虛繼承而來的子類會生成一個隱藏的虛基類指標(vbptr),在Microsoft Visual C++中,虛基類表指標總是在虛擬函式表指標之後,因而,對某個類例項來說,如果它有虛基類指標,那麼虛基類指標可能在例項的0位元組偏移處(該類沒有vptr時,vbptr就處於類例項記憶體佈局的最前面,否則vptr處於類例項記憶體佈局的最前面),也可能在類例項的4位元組偏移處。
虛基類表也由多個條目組成,條目中存放的是偏移值。
第一個條目存放虛基類表指標(vbptr)所在地址到該類記憶體首地址的偏移值
第二、第三...個條目依次為該類的最左虛繼承父類、次左虛繼承父類...的記憶體地址相對於虛基類表指標的偏移值。
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 }
物件模型:
菱形虛繼承
菱形虛繼承是多繼承和虛繼承的複合,直接畫一個物件模型吧:
筆記原圖: