轉載出處:
https://www.cnblogs.com/hushpa/p/5707475.html
https://blog.csdn.net/lihao21/article/details/50688337
先看代碼:
#include <iostream> using namespace std; 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 Derive : public Base{ public: void g() {cout<<"derive::g"<<endl;} }; //可以稍後再看 int main () { cout<<"size of Base: "<<sizeof(Base)<<endl; typedef void(*Func)(void); Base b; Base *d = new Derive(); long* pvptr = (long*)d; long* vptr = (long*)*pvptr; Func f = (Func)vptr[0]; Func g = (Func)vptr[1]; Func h = (Func)vptr[2]; f(); g(); h(); return 0; }
都知道C++中的多態是用虛函數實現的: 子類覆蓋父類的虛函數, 然後聲明一個指向子類對象的父類指針, 如Base *b = new Derive();
當調用b->f()時, 調用的是子類的Derive::f()。
這種機制內部由虛函數表實現,下面對虛函數表結構進行分析,並且用GDB驗證。
1. 基礎知識:
(1) 32位os 指針長度爲4字節, 64位os 指針長度爲8字節, 下面的分析環境爲64位 linux & g++ 4.8.4.
(2) new一個對象時, 只爲類中成員變量分配空間, 對象之間共享成員函數。
2. _vptr
運行下上面的代碼發現sizeof(Base) = 8, 說明編譯器在類中自動添加了一個8字節的成員變量, 這個變量就是_vptr, 指向虛函數表的指針。
_vptr有些文章裏說gcc是把它放在對象內存的末尾,VC是放在開始, 我編譯是用的g++,驗證了下是放在開始的:
驗證代碼:取對象a的地址與a第一個成員變量n的地址比較,如果不等,說明對象地址開始放的是_vptr. 也可以用gdb直接print a 會發現_vptr在開始
class A { public: int n; virtual void Foo(void){} }; int main() { A a; char *p1 = reinterpret_cast<char*>(&a); char *p2 = reinterpret_cast<char*>(&a.n); if(p1 == p2) { cout<<"vPtr is in the end of class instance!"<<endl; }else { cout<<"vPtr is in the head of class instance!"<<endl; } return 1; }
(3) 虛函數表
包含虛函數的類纔會有虛函數表, 同屬於一個類的對象共享虛函數表, 但是有各自的_vptr.
虛函數表實質是一個指針數組,裏面存的是虛函數的函數指針。
Base中虛函數表結構:
Derive中虛函數表結構:
(4)驗證
運行上面代碼結果:
size of Base: 8
base::f
derive::g
base::h
說明Derive的虛函數表結構跟上面分析的是一致的:
d對象的首地址就是vptr指針的地址-pvptr,
取pvptr的值就是vptr-虛函數表的地址
取vptr中[0][1][2]的值就是這三個函數的地址
通過函數地址就直接可以運行三個虛函數了。
函數表中Base::g()函數指針被Derive中的Derive::g()函數指針覆蓋, 所以執行的時候是調用的Derive::g()
(5)多繼承
附 GDB調試: (1) #生成帶有調試信息的可執行文件 g++ test.cpp -g -o test (2) #載入test gdb test (3) #列出Base類代碼 (gdb) list Base 1 #include <iostream> 2 3 using namespace std; 4 5 class Base { 6 public: 7 virtual void f() {cout<<"base::f"<<endl;} 8 virtual void g() {cout<<"base::g"<<endl;} 9 virtual void h() {cout<<"base::h"<<endl;} 10 }; (4) #查看Base函數地址 (gdb) info line 7 Line 7 of "test.cpp" starts at address 0x400ac8 <Base::f()> and ends at 0x400ad4 <Base::f()+12>. (gdb) info line 8 Line 8 of "test.cpp" starts at address 0x400af2 <Base::g()> and ends at 0x400afe <Base::g()+12>. (gdb) info line 9 Line 9 of "test.cpp" starts at address 0x400b1c <Base::h()> and ends at 0x400b28 <Base::h()+12>. (5)#列出Derive代碼 (gdb) list Derive 7 virtual void f() {cout<<"base::f"<<endl;} 8 virtual void g() {cout<<"base::g"<<endl;} 9 virtual void h() {cout<<"base::h"<<endl;} 10 }; 11 12 class Derive : public Base{ 13 public: 14 void g() {cout<<"derive::g"<<endl;} 15 }; (6)#查看Derive函數地址 (gdb) info line 14 Line 14 of "test.cpp" starts at address 0x400b46 <Derive::g()> and ends at 0x400b52 <Derive::g()+12>. (7)#start執行程序,n單步執行 (gdb) start Temporary breakpoint 1, main () at test.cpp:19 19 cout<<"size of Base: "<<sizeof(Base)<<endl; (gdb) n size of Base: 8 22 Base b; (gdb) 23 Base *d = new Derive(); (gdb) 25 long* pvptr = (long*)d; (gdb) 26 long* vptr = (long*)*pvptr; (gdb) 27 Func f = (Func)vptr[0]; (gdb) 28 Func g = (Func)vptr[1]; (gdb) 29 Func h = (Func)vptr[2]; (gdb) 31 f(); (gdb) (8) #print d對象, 0x400c90爲成員變量_vptr的值,也就是函數表的地址 (gdb) p *d $4 = {_vptr.Base = 0x400c90 <vtable for Derive+16>} (gdb) p vptr $6 = (long *) 0x400c90 <vtable for Derive+16> (9) #查看函數表值,與之前查看函數地址一致 (gdb) p (long*)vptr[0] $9 = (long *) 0x400ac8 <Base::f()> (gdb) p (long*)vptr[1] $10 = (long *) 0x400b46 <Derive::g()> (gdb) p (long*)vptr[2] $11 = (long *) 0x400b1c <Base::h()>
另vptr. vtable內存位置, refer http://www.tuicool.com/articles/iUB3Ebi
補充:
虛表是屬於類的,而不是屬於某個具體的對象,一個類只需要一個虛表即可。同一個類的所有對象都使用同一個虛表。
爲了指定對象的虛表,對象內部包含一個虛表的指針,來指向自己所使用的虛表。爲了讓每個包含虛表的類的對象都擁有一個虛表指針,編譯器在類中添加了一個指針,*__vptr,用來指向虛表。這樣,當類的對象在創建時便擁有了這個指針,且這個指針的值會自動被設置爲指向類的虛表。
圖2:對象與它的虛表
上面指出,一個繼承類的基類如果包含虛函數,那個這個繼承類也有擁有自己的虛表,故這個繼承類的對象也包含一個虛表指針,用來指向它的虛表。
四、動態綁定
說到這裏,大家一定會好奇C++是如何利用虛表和虛表指針來實現動態綁定的。我們先看下面的代碼。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A {
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
};
class C: public B {
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
類A是基類,類B繼承類A,類C又繼承類B。類A,類B,類C,其對象模型如下圖3所示。
圖3:類A,類B,類C的對象模型
由於這三個類都有虛函數,故編譯器爲每個類都創建了一個虛表,即類A的虛表(A vtbl),類B的虛表(B vtbl),類C的虛表(C vtbl)。類A,類B,類C的對象都擁有一個虛表指針,*__vptr,用來指向自己所屬類的虛表。
類A包括兩個虛函數,故A vtbl包含兩個指針,分別指向A::vfunc1()和A::vfunc2()。
類B繼承於類A,故類B可以調用類A的函數,但由於類B重寫了B::vfunc1()函數,故B vtbl的兩個指針分別指向B::vfunc1()和A::vfunc2()。
類C繼承於類B,故類C可以調用類B的函數,但由於類C重寫了C::vfunc2()函數,故C vtbl的兩個指針分別指向B::vfunc1()(指向繼承的最近的一個類的函數)和C::vfunc2()。
雖然圖3看起來有點複雜,但是隻要抓住“對象的虛表指針用來指向自己所屬類的虛表,虛表中的指針會指向其繼承的最近的一個類的虛函數”這個特點,便可以快速將這幾個類的對象模型在自己的腦海中描繪出來。