深入探究c++虛函數表原理,多态的實作
- 虛函數表簡介
- 一、通過虛函數指針得到虛函數表
- 二、所有對象共享虛函數表
- 三、周遊虛函數表,調用相應的函數
- 四、繼承與多态的實作,子類的虛函數表
虛函數表簡介
虛函數(Virtual Function)是通過一張虛函數表來實作的。簡稱為V-Table。在這個表中,主要是一個類的虛函數的位址表,這張表解決了繼承、覆寫的問題,保證其真實反應實際的函數。這樣,在有虛函數的類的執行個體中配置設定了指向這個表的指針的記憶體,是以,當用父類的指針來操作一個子類的時候,這張虛函數表就顯得尤為重要了,它就像一個地圖一樣,指明了實際所應該調用的函數。
編譯器保證虛函數表的指針存在于對象執行個體中最前面的位置(這是為了保證取到虛函數表的有最高的性能——如果有多層繼承或是多重繼承的情況下)。 這意味着可以通過對象執行個體的位址得到這張虛函數表,然後就可以周遊其中函數指針,并調用相應的函數。
這裡需要注意的是每個類有且隻有一張虛函數表,供所有的對象共享,類在構造對象的時候,把指向虛函數表的指針(即虛指針)存放到對象記憶體開始的地方,即對象執行個體的位址,即類不管有多少的虛函數,都隻通過指找到虛函數表進行通路
一、通過虛函數指針得到虛函數表
設計Base類如下:
class Base {
public:
virtual void f() { cout << "Base::f()"<<endl; }
virtual void g() { cout << "Base::g()"<<endl; }
virtual void h() { cout << "Base::h()"<<endl; }
};
通過執行個體化對象然後取對象位址(即虛指針位址)得到虛函數表的位址
Base b,a;
cout << "Base虛函數表指針位址:" << (int*)&b << endl;
cout << "Base虛函數表位址:" << (int*)*(int*)(&b) << endl;
這裡需要明确一下:**對象的位址是指向虛函數表的虛指針的位址,*在32為編譯系統中将指針轉成(int )型,虛指針裡存放的才是虛函數的位址,繼續對虛指針解引用才得到虛函數表的位址,可以把虛函數表看成數組,即得到數組的首位址。
二、所有對象共享虛函數表
之前看網上部落格,對此有争議:究竟是每個類隻有一張表還是說每個對象都有分别有一張表,從類設計以及資源管理的角度,很明顯不可能每個對象都有一張表,對此,也比較容易的可以驗證
Base b,a;
//cout << "Base虛函數表指針位址:" << (int*)&b << endl;
//cout << "Base虛函數表位址:" << (int*)*(int*)(&b) << endl;
cout << "Base b 虛函數表位址:" << (int*)*(int*)(&b) << endl;
cout << "Base a 虛函數表位址:" << (int*)*(int*)(&a) << endl; // 結論:同一類不同對象的虛函數表是同一個,即同類對象共享一個虛函數表
cout << "sizeof Base:" << sizeof(Base) << endl; // 4
運作效果如下:
a,b兩個Base對象,虛函數表位址一模一樣,而且當對Base類進行sizeof操作的時候,32位編譯系統中才4個位元組,也就是一個指針的大小,即虛指針,也就是僅僅是虛函數表位址的大小,4個位元組。是以,驗證了結論,同一類不同對象的虛函數表是同一個,即同類對象共享一個虛函數表。
三、周遊虛函數表,調用相應的函數
前文說到,可以把虛函數表看成一個數組,那麼周遊虛函數表就是周遊數組的操作。虛函數表中存放的是一個個虛函數的函數指針,故我們定義函數指針以友善周遊:
既然虛函數表可以看成數組,那麼對數組的通路就有兩種方式:直接用下标進行通路,與用指針解引用進行通路,比如看個小例子
int x[5] = { 1,2,3,4,5 };
cout << x[1] << endl << *(x + 1) << endl;
結果是一樣的,是以,對虛函數表進行通路的代碼如下,将指針轉換成pFUN型,然後調用函數指針
// 虛函數表位址看成數組首位址,存放着一個個虛函數的指針
int* vtable_addr = (int*)*(int*)(&b);
pFUN p1 = (pFUN) vtable_addr[0];
pFUN p2 = (pFUN) *((int*)*(int*)(&b)+1);
pFUN p3 = (pFUN) *((int*)*(int*)(&b)+2);
p1();
p2();
p3();
運作結果如下:
到此位置,已經通路但虛函數,說來複雜,其實也簡單,看鄙人畫的下面一張圖:
四、繼承與多态的實作,子類的虛函數表
定義子類如下
class Son :public Base {
public:
virtual void s1() { cout << "Son::s1()" << endl; }
virtual void g() { cout << "Son::g()" << endl; }
};
class Daughter :public Base {
public:
virtual void d1() { cout << "Daughter::d1()" << endl; }
virtual void d2() { cout << "Daughter::d2()" << endl; }
};
其中,Son子類覆寫了Base的虛函數g,Daughter子類并沒有覆寫Base的任何虛函數,事實上,對于Daughter類來說,他的虛函數表有五個虛函數,分别是父類的三個虛函數f,g,h,以及自己的兩個虛函數d1,d2可以編寫代碼進行測試
編寫代碼進行測試:
cout << "------------------測試 Daughter---------------" << endl;
Daughter d;
int* vtable_dau_addr = (int*)*(int*)(&d);
for (int i = 0; i < 5; ++i) {
pFUN p = (pFUN)vtable_dau_addr[i];
p();
}
事實上,對于Son類來說,他的虛函數表有4個虛函數,分别是父類未被覆寫的2個虛函數f,h,以及覆寫了的父類虛函數g,以及自身的s1,在執行時,覆寫了父類的虛函數将執行覆寫後自身的函數
cout << "-----------------測試 Son----------------" << endl;
Son s;
int* vtable_son_addr = (int*)*(int*)(&s);
for (int i = 0; i < 4; ++i) {
pFUN p = (pFUN)vtable_son_addr[i];
p();
}
可以看出,所謂繼承,無非就是将父類的虛函數繼承到子類,即在子類的虛函數表中加入父類的虛函數。
所謂多态,無非是子類重載父類的虛函數後,在虛函數表中将同名父類的虛函數指針換成子類的虛函數指針,僅此而已。
cout << "------------------測試多态--------------" << endl;
Base* t = new Son();
t->f();
t->g();
完整的測試代碼如下,運作環境vs2019 debug x86
#include <iostream>
using namespace std;
typedef void (*pFUN)();
class Base {
public:
virtual void f() { cout << "Base::f()"<<endl; }
virtual void g() { cout << "Base::g()"<<endl; }
virtual void h() { cout << "Base::h()"<<endl; }
};
class Son :public Base {
public:
virtual void s1() { cout << "Son::s1()" << endl; }
virtual void g() { cout << "Son::g()" << endl; }
};
class Daughter :public Base {
public:
virtual void d1() { cout << "Daughter::d1()" << endl; }
virtual void d2() { cout << "Daughter::d2()" << endl; }
};
int main() {
Base b,a;
//int x[5] = { 1,2,3,4,5 };
//cout << x[1] << endl << *(x + 1) << endl;
//cout << "Base虛函數表指針位址:" << (int*)&b << endl;
//cout << "Base虛函數表位址:" << (int*)*(int*)(&b) << endl;
cout << "Base b 虛函數表位址:" << (void*)*(int*)(&b) << endl;
cout << "Base a 虛函數表位址:" << (void*)*(int*)(&a) << endl; // 結論:同一類不同對象的虛函數表是同一個,即同類對象共享一個虛函數表
cout << "sizeof Base:" << sizeof(Base) << endl; // 4
// 虛函數表位址看成數組首位址,存放着一個個虛函數的指針
int* vtable_addr = (int*)*(int*)(&b);
pFUN p1 = (pFUN) vtable_addr[0];
pFUN p2 = (pFUN) *((int*)*(int*)(&b)+1);
pFUN p3 = (pFUN) *((int*)*(int*)(&b)+2);
p1();
p2();
p3();
cout << "-----------------測試 Son----------------" << endl;
Son s;
int* vtable_son_addr = (int*)*(int*)(&s);
for (int i = 0; i < 4; ++i) {
pFUN p = (pFUN)vtable_son_addr[i];
p();
}
cout << "------------------測試 Daughter---------------" << endl;
Daughter d;
int* vtable_dau_addr = (int*)*(int*)(&d);
for (int i = 0; i < 5; ++i) {
pFUN p = (pFUN)vtable_dau_addr[i];
p();
}
cout << "------------------測試多态--------------" << endl;
Base* t = new Son();
t->f();
t->g();
system("pause");
return 0;
}