1,C++ 中繼承是非常重要的一個特性,本節課研究在繼承的情形下,C++ 的對象模 型又有什麼不同; 2,繼承對象模型(最簡單的情況下): 1,在 C++ 編譯器的內部類可以理解為結構體; 2,子類是由父類成員疊加子類新成員得到的; 1,代碼示例: 1 class Derived : public ...
1,C++ 中繼承是非常重要的一個特性,本節課研究在繼承的情形下,C++ 的對象模 型又有什麼不同;
2,繼承對象模型(最簡單的情況下):
1,在 C++ 編譯器的內部類可以理解為結構體;
2,子類是由父類成員疊加子類新成員得到的;
1,代碼示例:
1 class Derived : public Demo
2 {
3 int mk;
4 };
2,對象排布:
1,在對象模型中,先排布父類對象模型,再排布子類對象模型,見 本文3中內容;
3,繼承對象模型初探編程實驗:
1 #include <iostream> 2 #include <string> 3 4 using namespace std; 5 6 class Demo 7 { 8 protected: 9 int mi; 10 int mj; 11 public: 12 virtual void print() 13 { 14 cout << "mi = " << mi << ", " 15 << "mj = " << mj << endl; 16 } 17 }; 18 19 class Derived : public Demo 20 { 21 int mk; 22 public: 23 Derived(int i, int j, int k) 24 { 25 mi = i; 26 mj = j; 27 mk = k; 28 } 29 30 void print() 31 { 32 cout << "mi = " << mi << ", " 33 << "mj = " << mj << ", " 34 << "mk = " << mk << endl; 35 } 36 }; 37 38 struct Test 39 { 40 void* p; // 為了證明 C++ 編譯器真的會在對象中塞入一個指針成員變數,且指針放在最開始的位元組處; 41 int mi; 42 int mj; 43 int mk; 44 }; 45 46 int main() 47 { 48 cout << "sizeof(Demo) = " << sizeof(Demo) << endl; // 8 bytes 49 cout << "sizeof(Derived) = " << sizeof(Derived) << endl; // 12 bytes 50 51 Derived d(1, 2, 3); 52 Test* p = reinterpret_cast<Test*>(&d); 53 54 cout << "Before changing ..." << endl; 55 56 d.print(); // mi = 1, mj = 2, mk = 3; 57 58 /* 通過 p 對象改變成員變數的值,這裡加了 p 指針後任然能夠成功的訪問; */ 59 p->mi = 10; 60 p->mj = 20; 61 p->mk = 30; 62 63 cout << "After changing ..." << endl; 64 65 d.print(); // mi = 10, mj = 20, mk = 30;在外界訪問不到的保護成員變數的值被改變了,改變是因為 d 對象的記憶體分佈 Test 結構體的(此時類中未有虛函數,Test 中未有 空指針),因此可以用 p 指針改變 d 對象當中成員變數的值; 66 67 return 0; 68 }
4,多態對象模型:
1,C++ 多態的實現原理:
1,當類中聲明虛函數時,編譯器會在類中生成一個虛函數表;
2,虛函數表是一個存儲成員函數地址的數據結構;
1,存儲虛函數成員地址的數據結構;
3,虛函數表是由編譯器自動生成與維護的;
4,virtual 成員函數會被編譯器放入虛函數表中;
1,這個表是給對象使用的;
2,對象在創建時,在內部有一個虛函數表指針,這個指針指向虛函數表;
5,存在虛函數時,每個對象中都有一個指向虛函數表的指針;
2,框圖展示:
1,框架一
1,編譯父類時,編譯器發現了 virtual 成員函數,因此編譯器創建了一個虛函數表,並且將虛函數的地址放到了虛函數表裡面;
2,編譯子類時,繼承自 Demo,編譯器發現重寫了 add 函數,因此必須是虛函數,於是編譯器就為子類也生成一張虛函數表,並且也會在虛函數表中放入重寫過後的 add 虛函數的地址;
2,框架二
1,當創建父類對象的時候,會為 Demo 對象自動的塞入一個指針 VPTR,也 就是如果類中有虛函數的話,在最終生成類對象的時候,會被編譯器強 制賽一個指針成員變數,這個指針成員變數對於程式員是不可見的,但是它確確實實的會存在對象當中,這個指針成員變數指向了虛函數表;
2,當創建子類對象的時候,會為 Derived 對象自動的塞入一個指針 VPTR,其是一個虛函數表指針,最終會指向創建的虛函數表;
3,通過 p 指針來調用虛函數 add(),編譯器就會判斷,當前調用的 add() 函數是不是虛函數,如果是虛函數,編譯器肯定可以知道這個虛函數地址位於虛函數表裡面,編譯器根據 p 指向的實際對象通過強行塞入的指針來查找虛函數表,然後在虛函數表裡面取得具體的 add() 函數地址,然後通過這個地址來調用,這樣子就實現了多態;
4,當通過指針調用的函數不是虛函數,這時就不會查找虛函數表了,此時就能夠直接確定函數地址;
3,框架三
1,紅色箭頭代表定址操作,即代表確定最後 add() 地址的操作;
2,通過 p 指針找到具體的對象,然後通過具體的對象找到這個虛函數表指針,之後通過虛函數表指針找到虛函數表,在虛函數表裡面通過查找找到最後的函數地址;
3,多態發生的情形下,調用一個函數要經歷三次定址,這個調用效率不會高,即虛函數的調用效率低於普通的成員函數,C++ 中的多態是通過犧牲效率得到的;
4,所以在寫 C++ 面向對象程式的時候,要考慮一個成員函數有沒有必要成為虛函數,因為每當我們定義一個虛函數,就會犧牲一定的效率,而 C++ 因為繼承了 C 語言的特性,所以天生就要高效,既要高效,又要實現多態,這就交給了程式員了;
5,虛函數中的指針指向具體對象,具體對象指針指向虛函數表,虛函數表中的指針指向具體的虛函數實現函數;
5,多態本質分析編程實驗(用 C 實現多態):
1,51-2.h 文件:
1 #ifndef _51_2_H_ 2 #define _51_2_H_ 3 4 typedef void Demo; 5 typedef void Derived; // C 語言實現繼承用 C++ 中的方法,即疊加; 6 7 /* 父類中繼承的成員函數 */ 8 Demo* Demo_Create(int i, int j); 9 int Demo_GetI(Demo* pThis); 10 int Demo_GetJ(Demo* pThis); 11 int Demo_Add(Demo* pThis, int value); // 虛函數 12 void Demo_Free(Demo* pThis); 13 14 /* 子類中新定義的成員函數 */ 15 Derived* Derived_Create(int i, int j, int k); // 構造函數; 16 int Derived_GetK(Derived* pThis); 17 int Derived_Add(Derived* pThis, int value); // 虛函數 18 19 #endif
2,51-2.c 文件:
1 #include "51-2.h" 2 #include "malloc.h" 3 4 static int Demo_Virtual_Add(Demo* pThis, int value); // 父類,先在這裡聲明,實現見第六步; 5 static int Derived_Virtual_Add(Demo* pThis, int value); // 子類 3,聲明子類虛函數,實現見下麵 6 7 struct VTable // 2. 定義虛函數表數據結構(用結構體表示虛函數表的數據結構,其用來創建虛函數表,見 static struct VTable g_Demo_vtbl) 8 { 9 int (*pAdd)(void*, int); // 3. 虛函數表裡面存儲什麼? 10 }; 11 12 /* 父類成員函數 */ 13 struct ClassDemo 14 { 15 struct VTable* vptr; // 1. 定義虛函數表指針 ==》 虛函數表指針類型是什麼,見第二步定義; 16 int mi; 17 int mj; 18 }; 19 20 /* 子類成員函數 */ 21 struct ClassDerived 22 { 23 struct ClassDemo d; // 父類的成員變數疊加上子類的成員變數,最開始的部分為父類; 24 int mk; 25 }; 26 27 /* 父類,創建一個全局的虛函數表變數,通過 static 關鍵字將虛函數表隱藏在當前的文件中,外界不可訪問 */ 28 static struct VTable g_Demo_vtbl = 29 { 30 Demo_Virtual_Add // 7,用真正意義上的虛函數來初始化虛函數表指針; 31 }; 32 33 /* 子類 2 放子類真正意義上的虛函數 */ 34 static struct VTable g_Derived_vtbl = // static 關鍵字是對虛函數表這個變數隱藏在當前文件當中,完結不可訪問。 35 { 36 Derived_Virtual_Add 37 }; 38 39 /* 父類構造函數 */ 40 Demo* Demo_Create(int i, int j) 41 { 42 struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo)); 43 44 if( ret != NULL ) 45 { 46 ret->vptr = &g_Demo_vtbl; // 4. 關聯對象和虛函數表 47 ret->mi = i; 48 ret->mj = j; 49 } 50 51 return ret; 52 } 53 54 /* 父類成員函數 */ 55 int Demo_GetI(Demo* pThis) 56 { 57 struct ClassDemo* obj = (struct ClassDemo*)pThis; 58 59 return obj->mi; 60 } 61 62 /* 父類成員函數 */ 63 int Demo_GetJ(Demo* pThis) 64 { 65 struct ClassDemo* obj = (struct ClassDemo*)pThis; 66 67 return obj->mj; 68 } 69 70 // 6. 定義虛函數表中指針所指向的具體函數 71 static int Demo_Virtual_Add(Demo* pThis, int value) 72 { 73 struct ClassDemo* obj = (struct ClassDemo*)pThis; 74 75 return obj->mi + obj->mj + value; 76 } 77 78 /* 這個函數功能和上個函數功能並沒有重覆,這個函數變成對外的用戶所使用的函數介面 */ 79 // 5. 分析具體的虛函數是什麼?要定義一個全局意義上的真正的虛函數,並且這個虛函數只在當前文件中可以訪問; 80 int Demo_Add(Demo* pThis, int value) 81 { 82 struct ClassDemo* obj = (struct ClassDemo*)pThis; 83 84 /* 通過對象找到具體的虛函數表指針,然後再找到具體的 add() 函數,具體的 add() 函數地址保存在 pAdd 裡面,在這裡應該是 Demo_Virtual_Add()函數 */ 85 return obj->vptr->pAdd(pThis, value); 86 } 87 88 /* 父類析構函數 */ 89 void Demo_Free(Demo* pThis) 90 { 91 free(pThis); 92 } 93 94 /* 子類構造函數 */ 95 Derived* Derived_Create(int i, int j, int k) 96 { 97 struct ClassDerived* ret = (struct ClassDerived*)malloc(sizeof(struct ClassDerived)); 98 99 if( ret != NULL ) 100 { 101 ret->d.vptr = &g_Derived_vtbl; // 子類 1 ,首先關聯虛函數表指針,指向子類虛函數表; 102 ret->d.mi = i; // 初始化父類成員變數,d 是子類中父類的結構體變數; 103 ret->d.mj = j; 104 ret->mk = k; 105 } 106 107 return ret; 108 } 109 110 /* 子類成員函數 */ 111 int Derived_GetK(Derived* pThis) 112 { 113 struct ClassDerived* obj = (struct ClassDerived*)pThis; 114 115 return obj->mk; 116 } 117 118 /* 子類成員函數 */ 119 static int Derived_Virtual_Add(Demo* pThis, int value) 120 { 121 struct ClassDerived* obj = (struct ClassDerived*)pThis; 122 123 return obj->mk + value; 124 } 125 126 /* 子類成員函數 */ 127 int Derived_Add(Derived* pThis, int value) 128 { 129 struct ClassDerived* obj = (struct ClassDerived*)pThis; 130 131 return obj->d.vptr->pAdd(pThis, value); 132 }
3,應用文件:
1 #include "stdio.h" 2 #include "51-2.h" 3 4 void run(Demo* p, int v) 5 { 6 int r = Demo_Add(p, v); // DEmo_Add(p, 3); 沒有實現多態的時候,C++ 編譯器這樣做更安全; 7 8 printf("r = %d\n", r); 9 } 10 11 int main() 12 { 13 Demo* pb = Demo_Create(1, 2); 14 Derived* pd = Derived_Create(1, 22, 333); 15 16 printf("pb->add(3) = %d\n", Demo_Add(pb, 3)); // 6 17 printf("pd->add(3) = %d\n", Derived_Add(pd, 3)); // 336 18 19 run(pb, 3); // 沒有實現多態的時候,列印 6;實現多態後,列印 6; 20 run(pd, 3); // 沒有實現多態的時候,列印 26;實現多態後,列印 336; 21 22 Demo_Free(pb); 23 Demo_Free(pd); // 子類可以繼承父類的析構函數,所以可以通過父類的析構函數來析構子類對象; 24 25 return 0; 26 }
4,步驟:
1,先實現基本的子類繼承和其成員函數基本功能;
2,後實現多態;
5,C 實現 C++ 中的多態(第三個視頻這裡不是很明白):
1,子類繼承:
1,另外生成結構體,內容由子類疊加父類的結構體內容;
2,子類構造函數:
1,另外寫,先在堆上面生成指向結構體的指針,子類調用父類的構造函數是不影響父類原來的構造函數的;
3,多態實現:
1,在對象的結構體中定義虛函數表指針(要考慮虛函數表指針類型);
2,在虛函數結構體中定義虛函數表數據結構(就是定義一個空的結構體);
3,在虛函數結構表中存放指向虛函數成員函數的指針;
4,在構造函數中關聯具體的對象和虛函數表;
5,分析讓那個函數稱為真正的虛函數( static 修飾 );
6,定義虛函數表指針所指向的具體函數。
6,小結:
1,繼承的本質就是父子間成員變數的疊加;
2,C++ 中的多態是通過虛函數表實現的;
3,虛函數表是由編譯器自動生成與維護的;
4,虛函數的調用效率低於普通成員函數;