一、多態與重載
1、多態的概念
面向對象的語言有三大特性:繼承、封裝、多態。虛函數作為多態的實現方式,重要性毋庸置疑。
多態意指相同的消息給予不同的對象會引發不同的動作(一個接口,多種方法)。其實更簡單地來說,就是“在用父類指針調用函數時,實際調用的是指針指向的實際類型(子類)的成員函數”。多態性使得程序調用的函數是在運行時動態確定的,而不是在編譯時靜態確定的。
2、重載—編譯期多態的體現
重載,是指在一個類中的同名不同參數的函數調用,這樣的方法調用是在編譯期間確定的。
3、虛函數—運行期多態的體現
運行期多態發生的三個條件:繼承關系、虛函數覆蓋、父類指針或引用指向子類對象。
當使用類的指針調用成員函數時,普通函數由指針類型決定,而虛函數由指針指向的實際類型決定。虛函數的實現過程:通過對象內存中的vptr找到虛函數表vtbl,接着通過vtbl找到對應虛函數的實現區域並進行調用。
二、虛函數的實現(內存布局)
虛函數表中只存有一個虛函數的指針地址,不存放普通函數或是構造函數的指針地址。只要有虛函數,C++類都會存在這樣的一張虛函數表,不管是普通虛函數亦或是純虛函數,亦或是派生類中隱式聲明的這些虛函數都會生成這張虛函數表。
虛函數表創建的時間:在一個類構造的時候,創建這張虛函數表,而這個虛函數表是供整個類所共有的。虛函數表存儲在對象最開始的位置。虛函數表其實就是函數指針的地址。函數調用的時候,通過函數指針所指向的函數來調用函數。
1.無繼承情況
點擊查看代碼
#include <iostream>
using namespace std;
class Base
{
public:
Base(){cout<<"Base construct"<<endl;}
virtual void f() {cout<<"Base::f()"<<endl;}
virtual void g() {cout<<"Base::g()"<<endl;}
virtual void h() {cout<<"Base::h()"<<endl;}
virtual ~Base(){}
};
int main()
{
typedef void (*Fun)(); //定義一個函數指針類型變量類型 Fun
Base *b = new Base();
//虛函數表存儲在對象最開始的位置
//將對象的首地址輸出
cout<<"首地址:"<<*(int*)(&b)<<endl;
Fun funf = (Fun)(*(int*)*(int*)b);
Fun fung = (Fun)(*((int*)*(int*)b+1));//地址內的值 即為函數指針的地址,將函數指針的地址存儲在了虛函數表中了
Fun funh = (Fun)(*((int *)*(int *)b+2));
funf();
fung();
funh();
cout<<(Fun)(*((int*)*(int*)b+4))<<endl; //最后一個位置為0 表明虛函數表結束 +4是因為定義了一個 虛析構函數
delete b;
return 0;
}

2、單繼承情況(無虛函數覆蓋)
假設有如下所示的一個繼承關系:

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

【Note】:
a.虛函數按照其聲明順序放於表中。
b.父類的虛函數在子類的虛函數前面。
3、單繼承情況(有虛函數覆蓋)
覆蓋父類的虛函數是很顯然的事情,不然,虛函數就變得毫無意義。下面,我們來看一下,如果子類中有虛函數重載了父類的虛函數,會是一個什么樣子?假設,我們有下面這樣的一個繼承關系。

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

【Note】:
a.覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。
b.沒有被覆蓋的函數依舊在原來的位置。
這樣,我們就可以看到對於下面這樣的程序,
Base *b = new Derive(); b->f();
1 2
由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,於是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。
4、多重繼承情況(無虛函數覆蓋)
下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關系。注意:子類並沒有覆蓋父類的函數。

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

【Note】:
a.每個父類都有自己的虛表(有幾個基類就有幾個虛函數表)。
b.子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)。
5、多重繼承情況(有虛函數覆蓋)
下面我們再來看看,如果發生虛函數覆蓋的情況。下圖中,我們在子類中覆蓋了父類的f()函數。

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

我們可以看見,三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,我們就可以任一靜態類型的父類來指向子類,並調用子類的f()了。如:
點擊查看代碼
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
三、虛函數的注意事項
1、構造函數不能是虛函數。
2、析構函數可以是虛函數且推薦最好設置為虛函數。
感謝:
https://blog.csdn.net/daaikuaichuan/article/details/88364336
