分析多繼承下的動態多態。
一、首先我們先了解一下三個概念:
1.重載。2.隱藏。3.覆蓋(重寫)
如何實現重載?——2個條件:
1-在同一作用域內。
2-兩個函數函數名相同,參數不同,返回值可以不同。
此時兩個函數就實現了重載,當然這是C++對於C特有的,因為C的時候對參數並沒有太多的考慮,C++的編譯器在編譯時對函數進行了重命名,所以就算是函數名相同的函數,如果參數不同,就會是不同的函數,對應不同的情況。
如何實現隱藏/重定義?——2個條件:
1-在不同作用域下,大多在繼承上體現。
2-函數名相同即可。
例如在 B類公有繼承了A類,此時B類和A類都聲明並定義了一個fun()公有成員函數,雖然B類也能獲得A中的fun()函數,但是因為B中也有一個同名函數,所以對fun()函數進行了重定義,A類的fun()函數依然存在,但是如果通過B類實例化對象來調用fun()函數,只能得到B類fun()函數。
如何實現覆蓋/重寫?——3個條件:
1-不在同一作用域下。(分別在基類和繼承類)
2-函數名相同,參數相同。訪問修飾可以不相同
3-基類函數必須與virtual關鍵字。
如果加了virtual關鍵字,那麽此時A類就會生成一個虛函數表。他的繼承類也會生成一個虛函數表,每個對象裏都會有一個虛函數表指針,虛函數表指針指向每個對象對應的虛函數表。從而達到A類調用A的fun()函數,B類調用B的fun()函數的效果。但是此時每個虛函數都發生了函數覆蓋,也就是說通過A類對象調用B類的fun()函數是實現不到的,反之亦然。
二、接下來分析第二個概念:
動態多態和靜態多態。
何謂靜態多態?
靜態多態就是通過函數重載實現,靜態多態是在編譯期完成,所以其效率也很高。在同一函數名的條件下,通過賦予不同參數來達到不同效果。這就是靜態多態,很重要的一點是靜態多態通過模板編程為C++帶來了泛型設計的概念(全特化,偏特化),例如STL庫。
何謂動態多態?
動態多態是通過虛函數表和繼承實現的。關於繼承需要記得賦值兼容規則裏的一條重要原則:父類指針可以指向子類對象。虛函數表是屬於類的,存在虛函數表的類會在實例化對象的時候給對象分配一個虛函數表指針(這也可以接受為什麽只有構造函數和定義為純虛函數的析構函數的類大小為4——切記,純虛函數的類無法實例化出對象)。
編譯器能夠通過對象內部得虛函數表指針找到對應的虛函數表,並調取相對應的函數。
三、開始我們今天的主題——多繼承下的動態多態:
1-重復繼承
要理解重復繼承,說不如看代,先看如下代碼:
1 class First 2 { 3 public: 4int if_; 5 char cf_; 6 public: 7 First() 8 :if_(1) 9 , cf_(‘F‘) 10 { 11 cout << "First" << endl; 12 } 13 virtual void f() 14 { 15 cout << "First_f()" << endl; 16 } 17 virtual void Bf() 18 { 19 cout << "First_Bf()" << endl; 20 } 21 };
這裏我們定義了一個First類當做基類,包含一個構造函數和兩個虛函數,讓我們接著看:
1 class Second_1 :public First 2 { 3 public: 4 int is_1; 5 char cs_1; 6 public: 7 Second_1() 8 :is_1(11) 9 , cs_1(‘S‘) 10 { 11 cout << "Second_1" << endl; 12 } 13 virtual void f() 14 { 15 cout << "Second_1_f()" << endl; 16 } 17 virtual void f1() 18 { 19 cout << "Second_1_f1()" << endl; 20 } 21 virtual void Bf1() 22 { 23 cout << "Second_1_Bf1()" << endl; 24 } 25 }; 26 27 class Second_2 :public First 28 { 29 public: 30 int is_2; 31 char cs_2; 32 public: 33 Second_2() 34 :is_2(21) 35 , cs_2(‘e‘) 36 { 37 cout << "Second_2" << endl; 38 } 39 virtual void f() 40 { 41 cout << "Second_2_f()" << endl; 42 } 43 virtual void f2() 44 { 45 cout << "Second_2_f2()" << endl; 46 } 47 virtual void Bf2() 48 { 49 cout << "Second_2_Bf2()" << endl; 50 } 51 };
之後我們分別定義了一個Second_1類和Second_2類來繼承First類,分別包含各自的構造函數,一個與基類同名的虛函數,兩個各自特有的虛函數,好嘞,接下來:
1 class Third :public Second_1,public Second_2 2 { 3 public: 4 int it; 5 char ct; 6 public: 7 Third() 8 :it(31) 9 , ct(‘T‘) 10 { 11 cout << "Third" << endl; 12 } 13 virtual void f() 14 { 15 cout << "Third_f()" << endl; 16 } 17 virtual void f1() 18 { 19 cout << "Third_f1()" << endl; 20 } 21 virtual void f2() 22 { 23 cout << "Third_f2()" << endl; 24 } 25 virtual void Bf3() 26 { 27 cout << "Third_f3()" << endl; 28 } 29 };
最後我們定義一個Third類,包含一個構造函數,一個與基類同名的虛函數,一個與Second_1類同名的虛函數,一個與Second_2類同名的虛函數,一個自己特有的虛函數。
為了研究方便,我把所有的繼承和成員屬性都設定為了public。讓我們用一個圖來解釋一下上述的繼承關系:
可以很清晰的看到,Third類所繼承的Second_1類與Second_2類分別包含了一份First類,其中一份或者兩份可能為拷貝,那麽此時如果我Third的對象想要調用First類的成員就會產生二義性,因為編譯器無法分辨你究竟想要調用哪個派生類裏的基類成員,這,就是重復繼承。
針對上述代碼,我編寫了如下main()函數:
1 int main() 2 { 3 Third th; 4 Second_1 se_1; 5 Second_2 se_2; 6 7 Print(*((int *)&th)); 8 9 getchar(); 10 return 0; 11 }
Print函數我們先放一邊,接著會講到。
我分別實例化了三個對象:Third類對象th,Second_1類對象se_1,Second_2類對象se_2。讓我們按F10,程序跑起來。
首先,我們定義完三個對象,我們得到:
這裏的顯示不用過多解釋,首先實例化Third類的對象,調用Third的構造函數,當Third有父類時,先調用父類構造函數,首先調用Second_1,Second_1又要向上調用First,再回來Third還要調用Second_2,Second_2調用First,從而得到如上結果。
我們打開內存,直接來看對象th對象內部得東西:
這裏面我們能看到一些熟悉的值,根據大小端得到它們分別為0x00000001=1,0x0000000b=11,0x00000015=21,0x0000001f=31。再回過頭來看我們上面的構造函數,1,11,21,31分別是First類中的if_、Second_1類中的is_1、Second_2類中的is_2、Third類中的it。
再讓我們打開監視,來看一下th內部__vfptr虛函數表指針的地址:
不妨猜測一下,這Third類對象th中的內容是什麽,根據我們上述得到的規律:
這裏__vfptr代表虛函數表指針,因為是重復繼承,我們可以得到兩個上圖中的兩個虛函數表指針分別指向不同的兩個虛函數表,而且虛函數表中函數如同我們猜測。
如何驗證我們的猜測?這裏就用到了我們的Print()函數,我們根據虛函數表指針是對象的第一個成員的規律來寫出如下代碼:
1 typedef void(*PFUN)(); 2 void Print(int &p) 3 { 4 int *pf = (int *)p; 5 while (* pf) 6 { 7 PFUN pfun = (PFUN)*pf; 8 pfun(); 9 pf++; 10 } 11 }
我們利用一個函數指針來指向我們的虛函數表,從而輸出虛函數表中的函數,我們利用這個規律,首先檢驗&th處,也就是Second_1類中__vfptr指向的地址——Second_1的虛函數表。
得到如下結果:
事實證明是對的,Second_1類的虛函數表中的 f() 函數和 f1() 函數都發生了覆蓋。
我們再加上如下代碼:
1 cout << "-------------------" << endl;
2 Print(*((int *)&th+5));
進一步驗證了我們的猜想:重復繼承下下虛函數表不再是放到基類裏,因為這樣會存在二義性,而是放在Second_1類和Second_2類中,他們分別發生了部分函數的覆蓋。
我們也看到了C++的不安全,如果給我一個虛函數表位置,我就可以訪問到父類的自己擁有的虛函數,這是非常不安全的。
2-菱形繼承
要想研究菱形繼承,我們只需將上述的Second_1類和Second_2類的繼承方式前加上virtual即可:
1 class Second_1 :virtual public First 2 { 3 public: 4 int is_1; 5 char cs_1; 6 public: 7 Second_1() 8 :is_1(11) 9 , cs_1(‘S‘) 10 { 11 cout << "Second_1" << endl; 12 } 13 virtual void f() 14 { 15 cout << "Second_1_f()" << endl; 16 } 17 virtual void f1() 18 { 19 cout << "Second_1_f1()" << endl; 20 } 21 virtual void Bf1() 22 { 23 cout << "Second_1_Bf1()" << endl; 24 } 25 }; 26 27 class Second_2 :virtual public First 28 { 29 public: 30 int is_2; 31 char cs_2; 32 public: 33 Second_2() 34 :is_2(21) 35 , cs_2(‘e‘) 36 { 37 cout << "Second_2" << endl; 38 } 39 virtual void f() 40 { 41 cout << "Second_2_f()" << endl; 42 } 43 virtual void f2() 44 { 45 cout << "Second_2_f2()" << endl; 46 } 47 virtual void Bf2() 48 { 49 cout << "Second_2_Bf2()" << endl; 50 } 51 };
此時讓我們再打開看th的內存:
這裏我們還是能看到我們熟悉的身影:0x0000000b=11,0x00000015=21,0x00000001f=31,0x00000001=1。
繼續打開我們的監視,我們可以看到th對象內部的__vfptr虛函數表指針的位置:
我們繼續根據這些來得到我們的內存猜測:
我們先把那兩個地址以及那個0x0000000放到一邊,首先那三個__vfptr指針是否屬實——
繼續利用Print()函數和如下代碼:
1 Print(*(int *)&th); 2 cout << "--------------" << endl; 3 Print(*((int *)&th + 4)); 4 cout << "--------------" << endl; 5 Print(*((int *)&th + 11));
得到如下結果:
接下來,我們繼續探討那兩個地址,我將地址輸入到內存窗口,得到如下結果:
不難看出,第一個數據都為-4,第二個數據一個為0x00000028=40,一個為0x00000018=24,這兩個數字代表什麽?
我們再回到內存,不難發現在第一個地址處動40個字節,也就是10個地址就能找到屬於First類的虛函數表指針,再第二個指針處動24個字節,也就是6個地址就能找到屬於First類的虛函數表指針。
那我們就可以完善我們的猜測:(0x0000000在我估計是一個結束標識,並未深究,輸在抱歉)
所以可以看出,虛繼承在消除二義性上起到了關鍵的作用,從直接給派生類兩份備份的簡單粗暴變成了分別給了一個地址讓派生類可以通過地址來找到基類並且調用相關的成員函數和數據成員,還減少了數據冗余,這也是C++令人著迷的地方。
文筆不好,感謝審閱。
分析多繼承下的動態多態。