一、多態與重載
1、多態的概念
面向對象的語言有三大特性:繼承、封裝、多態。虛函數作為多態的實現方式,重要性毋庸置疑。
多態意指相同的消息給予不同的對象會引發不同的動作(一個接口,多種方法)。其實更簡單地來說,就是“在用父類指針調用函數時,實際調用的是指針指向的實際類型(子類)的成員函數”。多態性使得程序調用的函數是在運行時動態確定的,而不是在編譯時靜態確定的。
2、重載—編譯期多態的體現
重載,是指在一個類中的同名不同參數的函數調用,這樣的方法調用是在編譯期間確定的。
3、虛函數—運行期多態的體現
運行期多態發生的三個條件:繼承關系、虛函數覆蓋、父類指針或引用指向子類對象。
二、虛函數實例
#include <iostream> #include <conio.h> using namespace std; class Base { public: virtual void vir_fun() { cout << "vitrual function,this is class Bass" <<endl;} void fun(){ cout << "normal function,this is class Bass" <<endl;} }; class A : public Base { public: virtual void vir_fun() { cout << "vitrual function,this is class A" <<endl;} void fun(){ cout << "normal function,this is class A" <<endl;} }; class B : public Base { public: virtual void vir_fun() { cout << "vitrual function,this is class B" <<endl;} void fun(){ cout << "normal function,this is class B" <<endl;} }; int main() { Base * b1 = new (Base); Base *b2 = new (A); Base *b3 = new (B); b1->fun(); //調用的都是基類base的函數 b2->fun(); //調用的都是基類base的函數 b3->fun (); //調用的都是基類base的函數 cout << "############################## " << endl ; b1->vir_fun(); //調用的是指針指向的實際類型的函數 BASE b2->vir_fun(); //調用的是指針指向的實際類型的函數 A b3->vir_fun(); //調用的是指針指向的實際類型的函數 B cout << "############################## " << endl ; ((A*) b2)->vir_fun(); //A ((B *)b3)->vir_fun(); //B cout << "############################## " << endl ; ((A*) b2)->fun(); //A ((B *)b3)->fun(); //B //當使用類的指針調用成員函數時,普通函數由指針類型決定, //而虛函數由指針指向的實際類型決定 }
顯示的內容
/* 顯示內容 normal function,this is class Bass normal function,this is class Bass normal function,this is class Bass ############################## vitrual function,this is class Bass vitrual function,this is class A vitrual function,this is class B ############################## vitrual function,this is class A vitrual function,this is class B ############################## normal function,this is class A normal function,this is class B */
在上述例子中,我們首先定義了一個基類base,基類有一個名為vir_func的虛函數,和一個名為func的普通成員函數。而類A,B都是由類base派生的子類,並且都對成員函數進行了重載。然后我們定義三個base類型的指針Base、a、b分別指向類base、A、B。可以看到,當使用這三個指針調用func函數時,調用的都是基類base的函數。而使用這三個指針調用虛函數vir_func時,調用的是指針指向的實際類型的函數。最后,我們將指針b做強制類型轉換,轉換為A類型指針,然后分別調用func和vir_func函數,發現普通函數調用的是類A的函數,而虛函數調用的是類B的函數。
以上,我們可以得出結論當使用類的指針調用成員函數時,普通函數由指針類型決定,而虛函數由指針指向的實際類型決定。
虛函數的實現過程:通過對象內存中的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】:
-
覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。
-
沒有被覆蓋的函數依舊在原來的位置。
這樣,我們就可以看到對於下面這樣的程序,
Base *b = new Derive(); b->f();
由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,於是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。
4、多重繼承情況(無虛函數覆蓋)
下面,再讓我們來看看多重繼承中的情況,假設有下面這樣一個類的繼承關系。注意:子類並沒有覆蓋父類的函數。

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

Note】:
-
每個父類都有自己的虛表(有幾個基類就有幾個虛函數表)。
-
子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)。
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、構造函數為什么不能定義為虛函數
構造函數不能是虛函數。
首先,我們已經知道虛函數的實現則是通過對象內存中的vptr來實現的。而構造函數是用來實例化一個對象的,通俗來講就是為對象內存中的值做初始化操作。那么在構造函數完成之前,vptr是沒有值的,也就無法通過vptr找到作為虛函數的構造函數所在的代碼區。
2、析構函數為什么要定義為虛函數?
析構函數可以是虛函數且推薦最好設置為虛函數。
class B { public: B() { printf("B()\n"); } virtual ~B() { printf("~B()\n"); } private: int m_b; }; class D : public B { public: D() { printf("D()\n"); } ~D() { printf("~D()\n"); } private: int m_d; }; int main() { B* pB = new D(); delete pB; return 0; }
C++中有這樣的約束:執行子類構造函數之前一定會執行父類的構造函數;同理,執行子類的析構函數后,一定會執行父類的析構函數,這也是為什么我們一直建議類的析構函數寫成虛函數的原因。
3、如何去驗證虛函數表的存在
typedef void(*Fun)(void); // 取類的一個實例 Base b; Fun pFun = NULL; // 把&b轉成int ,取得虛函數表的地址 cout << "虛函數表地址:" << (int*)(&b) << endl; // 再次取址就可以得到第一個虛函數的地址了 cout << "虛函數表 — 第一個函數地址:" << (int*)*(int*)(&b) << endl; pFun = (Fun)*((int*)*(int*)(&b)); pFun();
