c++語言虛函數實現多態的原理(更新版)


 自上一個帖子之間跳過了一篇總結性的帖子,之后再發,今天主要研究了c++語言當中虛函數對多態的實現,感嘆於c++設計者的精妙絕倫

c++中虛函數表的作用主要是實現了多態的機制。首先先解釋一下多態的概念,多態是c++的特點之一,關於多態,簡而言之就是 用父類的指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數,這種方法呢,可以讓父類的指針具有多種形態,也就是說不需要改動很多的代碼就可以讓父類這一種指針,干一些很多子類指針的事情,這里是從虛函數的實現機制層面進行研究

在寫這篇帖子之前對於相關的文章進行了查閱,基本上是大段的文字,所以我的這一篇可能會用大量的圖形進行贅述(如果理解有誤的地方,煩請大佬能夠指出),接下來就言歸正傳:

首先介紹一下為什么會引進多態呢,基於c++的復用性和拓展性而言,同類的程序模塊進行大量重復,是一件無法容忍的事情,比如我設置了蘋果,香蕉,西瓜類,現在想把這些東西都裝到碗這個函數里,那么在主函數當中,聲明對象是必須的,但是每一次裝進碗里對於水果來說,都要用自己的指針調用一次裝的功能,那為什么不把這些類抽象成一個水果類呢,直接定義一個水果類的指針一次性調用所有水果裝的功能呢,這個就是利用父類指針去調用子類成員,但是這個思想受到了指針指向類型的限制,也就是說表面指針指向了子類成員,但實際上還是只能調用子類成員里的父類成員,這樣的思想就變的毫無意義了,如果想要解決這個問題,只要在父類前加上virtual就可以解決了,這里就是利用虛函數實現多態的實例。

首先還是作為舉例來兩個類,在之前基礎知識的帖子中提到過,空類的大小是一個字節(占位符),函數,靜態變量都在編譯期就形成了,不用類去分配空間,但是做一個小實驗,看一看在定義了虛函數之后,類的大小是多少呢

 1 #include<iostream>
 2 using namespace std;
 3 class CFather 
 4 {
 5 public:
 6     virtual void AA()  //虛函數標識符
 7     {
 8         cout << "CFather :: AA()" << endl;
 9     }
10     void BB()
11     {
12         cout << "CFather  :: BB()" << endl;
13     }
14 };
15 class CSon : public CFather
16 {
17 public:
18     void AA()
19     {
20         cout << "CSon :: AA()" << endl;
21     }
22     void BB()
23     {
24         cout << "CSon :: BB()" << endl;
25     }
26 };
27 int main()
28 {
29     cout << sizeof(CFather) << endl;           //測試加了虛函數的類
30 
31     system("pause");
32     return 0;
33 }

很明顯類里裝了一個 4個字節的東西,除了整形int,就是指針了,沒錯這里裝的就是函數指針

先把這個代碼,給抽象成圖形進行理解,在這CFather為A,CSon為B

 

 此時就是一個單純的繼承的情況,不存在虛函數,然后我new一個對象,A *p = new A;那么 p -> AA(),必然是指向A類中的AA()函數,那么函數的調用有兩種方式 一種函數名加()直接調用,一種是利用函數指針進行調用,在這里我想要調用子類的,就可以利用函數指針進行調用,假設出來兩個函數指針,來指向B類中的兩個成員函數,如果我父類想要調用子類成員,就可以通過 p指針去調用函數指針,再通過函數指針去調用成員函數

 

 每一個函數都可以用一個函數指針去指着,那么每一類中的函數指針都可以形成自己的一個表,這個就叫做虛函數表

 

 那么在創建對象后,為什么類中會有四個字節的內存空間呢?

C++的標准規格說明書中說到,編譯器必需要保證虛函數表的指針存在於對象中最前面的位置(這是為了保證正確取到虛函數的偏移量)。這意味着我們通過對象實例的地址得到這張虛函數表,然后就可以遍歷其中函數指針,並調用相應的函數。也就是說這四個字節的指針,代替了上圖中(p->*pfn)()的作用,指向了函數指針,也就是說,在使用了虛函數的父類成員函數,雖然寫的還是p->AA(),實際上卻是,(p->*(vfptr[0])),而指向哪個虛函數表就由,創建的對象來決定

至此,就能理解如何用虛函數這個機制來實現多態的了

下面,我將分別說明“無覆蓋”和“有覆蓋”時的虛函數表的樣子。沒有覆蓋父類的虛函數是毫無意義的。我之所以要講述沒有覆蓋的情況,主要目的是為了給一個對比。在比較之下,我們可以更加清楚地知道其內部的具體實現。

 

無虛數覆蓋

下面,再讓我們來看看繼承時的虛函數表是什么樣的。假設有如下所示的一個繼承關系:

請注意,在這個繼承關系中,子類沒有重載任何父類的函數。那么,在派生類的實例中,Derive d; 的虛函表:

我們可以看到下面幾點:

1)虛函數按照其聲明順序放於表中。

2)父類的虛函數在子類的虛函數前面。

 

有虛數覆蓋

覆蓋父類的虛函數是很顯然的事情,不然,虛函數就變得毫無意義。下面,我們來看一下,如果子類中有虛函數重載了父類的虛函數,會是一個什么樣子?假設,我們有下面這樣的一個繼承關系。

為了讓大家看到被繼承過后的效果,在這個類的設計中,我只覆蓋了父類的一個函數:f()。那么,對於派生類的實例,其虛函數表會是下面的一個樣子:

我們從表中可以看到下面幾點,

1)覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。

2)沒有被覆蓋的函數依舊。

這樣,我們就可以看到對於下面這樣的程序,

            Base *b = new Derive();

            b->f();

b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,於是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。

 

 

2019-05-28 00:15:30 編程小菜鳥自我反思,今天圖畫的太丑了,大家多多擔待,如果技術上有什么偏差,大家可以踴躍批評我,謝謝!!!

 /*==========================================================手動分割線================================================*/

感謝@奕韻風華提出的問題,現在將多繼承的虛函數實現多態的情況討論一下,另再加上從代碼層面上對這個機制有更深的理解

討論多繼承還是從有無虛函數覆蓋的情況來開始

無虛函數覆蓋

假設有下面這樣一個類的繼承關系。注意:子類並沒有覆蓋父類的函數。

對於子類實例中的虛函數表,是下面這個樣子:

我們可以看到:

1)  每個父類都有自己的虛表。

2)  子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)

這樣做就是為了解決不同的父類類型的指針指向同一個子類實例,而能夠調用到實際的函數。

 

有虛函數覆蓋

下圖中,我們在子類中覆蓋了父類的f()函數。

下面是對於子類實例中的虛函數表的圖:

我們可以看見,三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,我們就可以任一靜態類型的父類來指向子類,並調用子類的f()了。比如

 

 1  Derive d;
 2 
 3   Base1 *b1 = &d;
 4 
 5   Base2 *b2 = &d;
 6 
 7   Base3 *b3 = &d;
 8 
 9   b1->f(); //Derive::f()
10 
11   b2->f(); //Derive::f()
12 
13   b3->f(); //Derive::f()
14 
15  
16 
17   b1->g(); //Base1::g()
18 
19   b2->g(); //Base2::g()
20 
21   b3->g(); //Base3::g()

 

 

以上就是對多繼承情況的一種討論

那么再看看如何在代碼的層面上來驗證原理呢?

首先在主函數內聲明了一個父類的指針,指向子類對象,那么這個對這個父類指針解引用的話,就能得到一個vfptr指針,和父類,子類對象,但是現在我只需要指向虛函數的指針,那么就可以定義指針只取前四個字節,利用強轉,*(int *)p 這個得到了vfptr的地址,那么繼續想要獲得虛函數表中的虛函數,就是再次解引用了,但是如何進行偏移呢?在整形,浮點型里,指針的偏移量都是指針指向類型所覺得的,而這里是個函數指針,函數指針不允許利用指針指向類型來進行偏移量的取值,因為函數的類型大小是不確定的,但是我們知道,虛函數表里都是函數指針,指針的大小是確定的都是四個字節,那還可以繼續利用強轉,控制指針每次偏移四個字節,那么這個時候再進行解引用就是我們所取得函數的地址了,如果語言太贅述的話,可以看下面的例子

 1 //--------------------------------------------------
 2 #include <iostream>
 3 using namespace std;
 4 
 5 
 6 class CFather
 7 {
 8 public:
 9     virtual void AA()
10     {
11         cout << "CFather::AA" << endl;
12     }
13     virtual void BB()
14     {
15         cout << "CFather::BB" << endl;
16     }
17     virtual void CC()
18     {
19         cout << "CFather::BB" << endl;
20     }
21     void DD()
22     {
23         cout << "CFather::DD" << endl;
24     }
25 };
26 
27 class CSon:public CFather
28 {
29 public:
30     virtual void AA()
31     {
32         cout << "CSon::AA" << endl;
33     }
34     virtual void BB()
35     {
36         cout << "CSon::BB" << endl;
37     }
38     void DD()
39     {
40         cout << "CSon::DD" << endl;
41     }
42     virtual void EE()
43     {
44         cout << "CSon::EE" << endl;
45     }
46 };
47 
48 
49 int main()
50 {
51 
52     typedef void (*PFUN)();
53 
54     cout << sizeof(CFather) << endl;
55     CFather* p = new CSon;
56     PFUN aa = (PFUN)*((int*)*(int*)p+0);
57     PFUN bb = (PFUN)*((int*)*(int*)p+1);
58     PFUN cc = (PFUN)*((int*)*(int*)p+2);
59     PFUN dd = (PFUN)*((int*)*(int*)p+3);
60     PFUN ee = (PFUN)*((int*)*(int*)p+4);
61 
62 
63 
64     system("pause");
65     return 0;
66 }

通過監視就能直接看到(因為vs編譯器不允許,利用父類指針直接使用虛函數不覆蓋情況下的子類成員函數,利用這個方法也可以查看子類虛函數)

驗證了我上面敘述的原理,首先父類中先 生成了虛函數表,再繼承到子類當中,如果子類中有重載的函數,直接重寫,沒有的話直接添加

 

 

 

 

2019-05-29 14:07:19 編程小菜鳥自我反思,大佬可以提出自己的建議和意見,謝謝!!! 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM