深入浅出C++虚函数的vptr与vtable
1.基礎理論
為了實現虛函數,C ++使用一種稱為虛擬表的特殊形式的后期綁定。該虛擬表是用于解決在動態/后期綁定方式的函數調用函數的查找表。虛擬表有時會使用其他名稱,例如“vtable”,“虛函數表”,“虛方法表”或“調度表”
虛擬表實際上非常簡單,雖然用文字描述有點復雜。首先,每個使用虛函數的類(或者從使用虛函數的類派生)都有自己的虛擬表。該表只是編譯器在編譯時設置的靜態數組。虛擬表包含可由類的對象調用的每個虛函數的一個條目。此表中的每個條目只是一個函數指針,指向該類可訪問的派生函數。
其次,編譯器還會添加一個隱藏指向基類的指針,我們稱之為vptr。vptr在創建類實例時自動設置,以便指向該類的虛擬表。與this指針不同,this指針實際上是編譯器用來解析自引用的函數參數,vptr是一個真正的指針
因此,它使每個類對象的分配大一個指針的大小。這也意味著vptr由派生類繼承,這很重要。
2.實現與內部結構?
下面我們來看自動與手動操縱vptr來獲取地址與調用虛函數!
開始看代碼之前,為了方便大家理解,這里給出調用圖:
/*** @file vptr1.cpp* @brief C++虛函數vptr和vtable* 編譯:g++ -g -o vptr vptr1.cpp -std=c++11* @author 光城* @version v1* @date 2019-07-20*/#include <iostream> #include <stdio.h> using namespace std;/*** @brief 函數指針*/ typedef void (*Fun)();/*** @brief 基類*/ class Base { public:Base(){};virtual void fun1(){cout << "Base::fun1()" << endl;}virtual void fun2(){cout << "Base::fun2()" << endl;}virtual void fun3(){}~Base(){}; };/*** @brief 派生類*/ class Derived: public Base { public:Derived(){};void fun1(){cout << "Derived::fun1()" << endl;}void fun2(){cout << "DerivedClass::fun2()" << endl;}~Derived(){}; }; /*** @brief 獲取vptr地址與func地址,vptr指向的是一塊內存,這塊內存存放的是虛函數地址,這塊內存就是我們所說的虛表** @param obj* @param offset** @return*/ Fun getAddr(void* obj,unsigned int offset) {cout<<"======================="<<endl;void* vptr_addr = (void *)*(unsigned long *)obj; //64位操作系統,占8字節,通過*(unsigned long *)obj取出前8字節,即vptr指針printf("vptr_addr:%p\n",vptr_addr);/*** @brief 通過vptr指針訪問virtual table,因為虛表中每個元素(虛函數指針)在64位編譯器下是8個字節,因此通過*(unsigned long *)vptr_addr取出前8字節,* 后面加上偏移量就是每個函數的地址!*/void* func_addr = (void *)*((unsigned long *)vptr_addr+offset);printf("func_addr:%p\n",func_addr);return (Fun)func_addr; } int main(void) {Base ptr;Derived d;Base *pt = new Derived(); // 基類指針指向派生類實例Base &pp = ptr; // 基類引用指向基類實例Base &p = d; // 基類引用指向派生類實例cout<<"基類對象直接調用"<<endl;ptr.fun1();cout<<"基類對象調用基類實例"<<endl;pp.fun1();cout<<"基類指針指向派生類實例并調用虛函數"<<endl;pt->fun1();cout<<"基類引用指向派生類實例并調用虛函數"<<endl;p.fun1();// 手動查找vptr 和 vtableFun f1 = getAddr(pt, 0);(*f1)();Fun f2 = getAddr(pt, 1);(*f2)();delete pt;return 0; } 基類對象直接調用 Base::fun1() 基類對象調用基類實例 Base::fun1() 基類指針指向派生類實例并調用虛函數 Derived::fun1() 基類引用指向派生類實例并調用虛函數 Derived::fun1() ======================= vptr_addr:0x5555f5d20cf0 func_addr:0x5555f5b2005c Derived::fun1() ======================= vptr_addr:0x5555f5d20cf0 func_addr:0x5555f5b20094 DerivedClass::fun2()我們發現C++的動態多態性是通過虛函數來實現的。簡單的說,通過virtual函數,指向子類的基類指針可以調用子類的函數。例如,上述通過基類指針指向派生類實例,并調用虛函數,將上述代碼簡化為:
Base *pt = new Derived(); // 基類指針指向派生類實例 cout<<"基類指針指向派生類實例并調用虛函數"<<endl; pt->fun1();其過程為:首先程序識別出fun1()是個虛函數,其次程序使用pt->vptr來獲取Derived的虛擬表。第三,它查找Derived虛擬表中調用哪個版本的fun1()。這里就可以發現調用的是Derived::fun1()。因此pt->fun1()被解析為Derived::fun1()!
除此之外,上述代碼大家會看到,也包含了手動獲取vptr地址,并調用vtable中的函數,那么我們一起來驗證一下上述的地址與真正在自動調用vtable中的虛函數,比如上述pt->fun1()的時候,是否一致!
這里采用gdb調試,在編譯的時候記得加上-g。
通過gdb vptr進入gdb調試頁面,然后輸入b Derived::fun1對fun1打斷點,然后通過輸入r運行程序到斷點處,此時我們需要查看調用棧中的內存地址,通過disassemable fun1可以查看當前有關fun1中的相關匯編代碼,我們看到了0x0000000000400ea8,然后再對比上述的結果會發現與手動調用的fun1一致,fun2類似,以此證明代碼正確!
gdb調試信息如下:
(gdb) b Derived::fun1 Breakpoint 1 at 0x400eb4: file vptr1.cpp, line 23. (gdb) r Starting program: /home/light/Program/CPlusPlusThings/virtual/pure_virtualAndabstract_class/vptr 基類對象直接調用 Base::fun1() 基類引用指向派生類實例 Base::fun1() 基類指針指向派生類實例并調用虛函數Breakpoint 1, Derived::fun1 (this=0x614c20) at vptr1.cpp:23 23 cout << "Derived::fun1()" << endl; (gdb) disassemble fun1 Dump of assembler code for function Derived::fun1():0x0000000000400ea8 <+0>: push %rbp0x0000000000400ea9 <+1>: mov %rsp,%rbp0x0000000000400eac <+4>: sub $0x10,%rsp0x0000000000400eb0 <+8>: mov %rdi,-0x8(%rbp) => 0x0000000000400eb4 <+12>: mov $0x401013,%esi0x0000000000400eb9 <+17>: mov $0x602100,%edi0x0000000000400ebe <+22>: callq 0x4009d0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>0x0000000000400ec3 <+27>: mov $0x400a00,%esi0x0000000000400ec8 <+32>: mov %rax,%rdi0x0000000000400ecb <+35>: callq 0x4009f0 <_ZNSolsEPFRSoS_E@plt>0x0000000000400ed0 <+40>: nop0x0000000000400ed1 <+41>: leaveq 0x0000000000400ed2 <+42>: retq End of assembler dump. (gdb) disassemble fun2 Dump of assembler code for function Derived::fun2():0x0000000000400ed4 <+0>: push %rbp0x0000000000400ed5 <+1>: mov %rsp,%rbp0x0000000000400ed8 <+4>: sub $0x10,%rsp0x0000000000400edc <+8>: mov %rdi,-0x8(%rbp)0x0000000000400ee0 <+12>: mov $0x401023,%esi0x0000000000400ee5 <+17>: mov $0x602100,%edi0x0000000000400eea <+22>: callq 0x4009d0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>0x0000000000400eef <+27>: mov $0x400a00,%esi0x0000000000400ef4 <+32>: mov %rax,%rdi0x0000000000400ef7 <+35>: callq 0x4009f0 <_ZNSolsEPFRSoS_E@plt>0x0000000000400efc <+40>: nop0x0000000000400efd <+41>: leaveq 0x0000000000400efe <+42>: retq End of assembler dump.總結
以上是生活随笔為你收集整理的深入浅出C++虚函数的vptr与vtable的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c++ 纯虚函数和抽象类那些事(三)
- 下一篇: 虚函数理解