深入理解C++ 虛函數表
C++中的虛函數的作用主要是實現了多態的機制。關於多態,簡而言之就是用父類型別的指針指向其子類的實例,然后通過父類的指針調用實際子類的成員函數。
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()
這種技術可以讓父類的指針有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的代碼來實現可變的算法。比如:模板技術,RTTI技術,虛函數技術,要么是試圖做到在編譯時決議,要么試圖做到運行時決議。
本文將詳細介紹虛函數表的實現及其內存布局。
虛函數表概述
虛函數表是指在每個包含虛函數的類中都存在着一個函數地址的數組。當我們用父類的指針來操作一個子類的時候,這張虛函數表指明了實際所應該調用的函數。
C++的編譯器保證虛函數表的指針存在於對象實例中最前面的位置,這樣通過對象實例的地址得到這張虛函數表,然后就可以遍歷其中函數指針,並調用相應的函數。
按照上面的說法,來看一個實際的例子:
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "f()" << endl; }
virtual void g() { cout << "g()" << endl; }
virtual void h() { cout << "h()" << endl; }
};
int main()
{
Base t;
( ((void(*)())*((int*)(*((int*)&t)) + 0)) ) ();
( ((void(*)())*((int*)(*((int*)&t)) + 1)) ) ();
( ((void(*)())*((int*)(*((int*)&t)) + 2)) ) ();
return 0;
}
經過VS2017,x86測試:


我們成功地通過實例對象的地址,得到了對象所有的類函數。

main定義Base類對象t,把&b轉成int *,取得虛函數表的地址vtptr就是:(int*)(&t),然后再解引用並強轉成int * 得到第一個虛函數的地址,也就是Base::f()即(int*)(*((int*)&t)),那么,第二個虛函數g()的地址就是(int*)(*((int*)&t)) + 1,依次類推。
單繼承下的虛函數表
派生類未覆蓋基類虛函數
下面我們來看下派生類沒有覆蓋基類虛函數的情況,其中Base類延用上一節的定義。從圖中可看出虛函數表中依照聲明順序先放基類的虛函數地址,再放派生類的虛函數地址。

可以看到下面幾點:
1)虛函數按照其聲明順序放於表中。
2)父類的虛函數在子類的虛函數前面。
測試代碼:
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "f()" << endl; }
virtual void g() { cout << "g()" << endl; }
virtual void h() { cout << "h()" << endl; }
};
class Devired :public Base{
public:
virtual void x() { cout << "x()" << endl; }
};
int main()
{
Devired t;
(((void(*)()) *((int*)(*((int*)&t))))) ();
(((void(*)())*((int*)(*((int*)&t)) + 1))) ();
(((void(*)())*((int*)(*((int*)&t)) + 2))) ();
//(((void(*)())*((int*)(*((int*)&t)) + 3))) ();
return 0;
}
測試效果:

派生類覆蓋基類虛函數
再來看一下派生類覆蓋了基類的虛函數的情形,可見:
- 虛表中派生類覆蓋的虛函數的地址被放在了基類相應的函數原來的位置 (顯然的,不然虛函數失去意義)
- 派生類沒有覆蓋的虛函數延用基類的
測試代碼:
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << "f()" << endl; }
virtual void g() { cout << "g()" << endl; }
virtual void h() { cout << "h()" << endl; }
};
class Derive :public Base{
public:
virtual void x() { cout << "x()" << endl; }
virtual void f() { cout << "Derive::f()" << endl; }
};
int main()
{
Derive t;
(((void(*)()) *((int*)(*((int*)&t))))) ();
(((void(*)())*((int*)(*((int*)&t)) + 1))) ();
(((void(*)())*((int*)(*((int*)&t)) + 2))) ();
//(((void(*)())*((int*)(*((int*)&t)) + 3))) ();
return 0;
}
測試效果:


多繼承下的虛函數表
無虛函數覆蓋
如果是多重繼承的話,問題就變得稍微復雜一丟丟,主要有幾點:
- 每個基類都有自己的虛函數表
- 派生類的虛函數地址存依照聲明順序放在第一個基類的虛表最后(這點和單繼承無虛函數覆蓋相同),具體見下圖所示:

測試代碼
#include <iostream>
class Base
{
public:
Base(int mem1 = 1, int mem2 = 2) : m_iMem1(mem1), m_iMem2(mem2) { ; }
virtual void vfunc1() { std::cout << "In vfunc1()" << std::endl; }
virtual void vfunc2() { std::cout << "In vfunc2()" << std::endl; }
virtual void vfunc3() { std::cout << "In vfunc3()" << std::endl; }
private:
int m_iMem1;
int m_iMem2;
};
class Base2
{
public:
Base2(int mem = 3) : m_iBase2Mem(mem) { ; }
virtual void vBase2func1() { std::cout << "In Base2 vfunc1()" << std::endl; }
virtual void vBase2func2() { std::cout << "In Base2 vfunc2()" << std::endl; }
private:
int m_iBase2Mem;
};
class Base3
{
public:
Base3(int mem = 4) : m_iBase3Mem(mem) { ; }
virtual void vBase3func1() { std::cout << "In Base3 vfunc1()" << std::endl; }
virtual void vBase3func2() { std::cout << "In Base3 vfunc2()" << std::endl; }
private:
int m_iBase3Mem;
};
class Devired : public Base, public Base2, public Base3
{
public:
Devired(int mem = 7) : m_iMem1(mem) { ; }
virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }
private:
int m_iMem1;
};
int main()
{
// Test_3
Devired d;
int *dAddress = (int*)&d;
typedef void(*FUNC)();
/* 1. 獲取對象的內存布局信息 */
// 虛表地址一
int *vtptr1 = (int*)*(dAddress + 0);
int basemem1 = (int)*(dAddress + 1);
int basemem2 = (int)*(dAddress + 2);
int *vtpttr2 = (int*)*(dAddress + 3);
int base2mem = (int)*(dAddress + 4);
int *vtptr3 = (int*)*(dAddress + 5);
int base3mem = (int)*(dAddress + 6);
/* 2. 輸出對象的內存布局信息 */
int *pBaseFunc1 = (int *)*(vtptr1 + 0);
int *pBaseFunc2 = (int *)*(vtptr1 + 1);
int *pBaseFunc3 = (int *)*(vtptr1 + 2);
int *pBaseFunc4 = (int *)*(vtptr1 + 3);
(FUNC(pBaseFunc1))();
(FUNC(pBaseFunc2))();
(FUNC(pBaseFunc3))();
(FUNC(pBaseFunc4))();
// .... 后面省略若干輸出內容,可自行補充
return 0;
}
測試效果:

派生類覆蓋基類虛函數
我們再來看一下派生類覆蓋了基類的虛函數的情形,可見:
- 虛表中派生類覆蓋的虛函數的地址被放在了基類相應的函數原來的位置
- 派生類沒有覆蓋的虛函數延用基類的
代碼如下所示,注意這里只給出了類的定義,main函數的測試代碼與上節一樣:
class Devired : public Base, public Base2, public Base3
{
public:
Devired(int mem = 7) : m_iMem1(mem) { ; }
virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }
virtual void vfunc1() { std::cout << "In Devired vfunc1()" << std::endl; }
virtual void vBase2func1() { std::cout << "In Devired vfunc1()" << std::endl; }
private:
int m_iMem1;
};
測試效果

鑽石型虛繼承
該繼承還是遵循上述的所有原則,我們直接來測試。
測試代碼
// 測試四:鑽石型虛繼承
//虛基指針所指向的虛基表的內容:
// 1. 虛基指針的第一條內容表示的是該虛基指針距離所在的子對象的首地址的偏移
// 2. 虛基指針的第二條內容表示的是該虛基指針距離虛基類子對象的首地址的偏移
#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;
class B
{
public:
B() : _ib(10), _cb('B') {}
virtual void f()
{
cout << "B::f()" << endl;
}
virtual void Bf()
{
cout << "B::Bf()" << endl;
}
private:
int _ib;
char _cb;
};
class B1 : virtual public B
{
public:
B1() : _ib1(100), _cb1('1') {}
virtual void f()
{
cout << "B1::f()" << endl;
}
#if 1
virtual void f1()
{
cout << "B1::f1()" << endl;
}
virtual void Bf1()
{
cout << "B1::Bf1()" << endl;
}
#endif
private:
int _ib1;
char _cb1;
};
class B2 : virtual public B
{
public:
B2() : _ib2(1000), _cb2('2') {}
virtual void f()
{
cout << "B2::f()" << endl;
}
#if 1
virtual void f2()
{
cout << "B2::f2()" << endl;
}
virtual void Bf2()
{
cout << "B2::Bf2()" << endl;
}
#endif
private:
int _ib2;
char _cb2;
};
class D : public B1, public B2
{
public:
D() : _id(10000), _cd('3') {}
virtual void f()
{
cout << "D::f()" << endl;
}
#if 1
virtual void f1()
{
cout << "D::f1()" << endl;
}
virtual void f2()
{
cout << "D::f2()" << endl;
}
virtual void Df()
{
cout << "D::Df()" << endl;
}
#endif
private:
int _id;
char _cd;
};
int main(void)
{
D d;
cout << sizeof(d) << endl;
return 0;
}
測試效果
1>class D size(52):
1> +---
1> 0 | +--- (base class B1)
1> 0 | | {vfptr}
1> 4 | | {vbptr}
1> 8 | | _ib1
1>12 | | _cb1
1> | | <alignment member> (size=3)
1> | +---
1>16 | +--- (base class B2)
1>16 | | {vfptr}
1>20 | | {vbptr}
1>24 | | _ib2
1>28 | | _cb2
1> | | <alignment member> (size=3)
1> | +---
1>32 | _id
1>36 | _cd
1> | <alignment member> (size=3)
1> +---
1> +--- (virtual base B)
1>40 | {vfptr}
1>44 | _ib
1>48 | _cb
1> | <alignment member> (size=3)
1> +---
1>
1>D::$vftable@B1@:
1> | &D_meta
1> | 0
1> 0 | &D::f1
1> 1 | &B1::Bf1
1> 2 | &D::Df
1>
1>D::$vftable@B2@:
1> | -16
1> 0 | &D::f2
1> 1 | &B2::Bf2
1>
1>D::$vbtable@B1@:
1> 0 | -4
1> 1 | 36 (Dd(B1+4)B)
1>
1>D::$vbtable@B2@:
1> 0 | -4
1> 1 | 20 (Dd(B2+4)B)
1>
1>D::$vftable@B@:
1> | -40
1> 0 | &D::f
1> 1 | &B::Bf
1>
總結
幾個原則
單繼承
- 虛表中派生類覆蓋的虛函數的地址被放在了基類相應的函數原來的位置
- 派生類沒有覆蓋的虛函數就延用基類的。同時,虛函數按照其聲明順序放於表中,父類的虛函數在子類的虛函數前面。
多繼承
3. 每個基類都有自己的虛函數表
4. 派生類的虛函數地址存依照聲明順序放在第一個基類的虛表最后
安全性問題
當我們直接通過父類指針調用子類中的未覆蓋父類的成員函數,編譯器會報錯,但通過實驗,我們可以用對象的地址訪問到各個子類的成員函數,就違背了C++語義,操作會有一定的隱患,當我們使用時要注意這些危險的東西!
參考:
https://coolshell.cn/articles/12165.html
