虚函数的vptr与vtable 为了实现虚函数,C ++使用一种称为虚拟表的特殊形式的后期绑定。该虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表。虚拟表有时会使用其他名称,例如“vtable”,“虚函数表”,“虚方法表”或“调度表”。
首先,每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表 。该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数。
其次,编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针。
因此,它使每个类对象的分配大一个指针的大小。这也意味着vptr由派生类继承,这很重要。
实现与内部结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 #include <iostream> #include <stdio.h> using namespace std;typedef void (*Fun) () ;class Base {public : Base (){}; virtual void fun1 () { cout << "Base::fun1()" << endl; } virtual void fun2 () { cout << "Base::fun2()" << endl; } virtual void fun3 () {} ~Base (){}; }; class Derived : public Base {public : Derived (){}; void fun1 () { cout << "Derived::fun1()" << endl; } void fun2 () { cout << "DerivedClass::fun2()" << endl; } ~Derived (){}; }; Fun getAddr (void *obj, unsigned int offset) { cout << "=======================" << endl; void *vptr_addr = (void *)*(unsigned long *)obj; printf ("vptr_addr:%p\n" , vptr_addr); 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 (); Fun f1 = getAddr (pt, 0 ); (*f1)(); Fun f2 = getAddr (pt, 1 ); (*f2)(); delete pt; return 0 ; }
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 基类对象直接调用 Base::fun1() 基类对象调用基类实例 Base::fun1() 基类指针指向派生类实例并调用虚函数 Derived::fun1() 基类引用指向派生类实例并调用虚函数 Derived::fun1() ======================= vptr_addr:0x559376928d50 func_addr:0x55937692656e Derived::fun1() ======================= vptr_addr:0x559376928d50 func_addr:0x5593769265a6 DerivedClass::fun2()
C++的动态多态性是通过虚函数来实现的。简单的说,通过virtual函数,指向子类的基类指针可以调用子类的函数。例如,上述通过基类指针指向派生类实例,并调用虚函数,将上述代码简化为:
1 2 3 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调试信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 Reading symbols from vptr... (gdb) b Derived::fun1 Breakpoint 1 at 0x157a: file vptr1.cpp, line 28. (gdb) r Starting program: /home/ansore/study/C++/basic_content/vptr/vptr 基类对象直接调用 Base::fun1() 基类对象调用基类实例 Base::fun1() 基类指针指向派生类实例并调用虚函数 Breakpoint 1, Derived::fun1 (this=0x55555556aeb0) at vptr1.cpp:28 28 void fun1() { cout << "Derived::fun1()" << endl; } (gdb) disassemble fun1 Dump of assembler code for function _ZN7Derived4fun1Ev: 0x000055555555556e <+0>: push %rbp 0x000055555555556f <+1>: mov %rsp,%rbp 0x0000555555555572 <+4>: sub $0x10,%rsp 0x0000555555555576 <+8>: mov %rdi,-0x8(%rbp) => 0x000055555555557a <+12>: lea 0xaa2(%rip),%rsi # 0x555555556023 0x0000555555555581 <+19>: lea 0x2af8(%rip),%rdi # 0x555555558080 <_ZSt4cout@GLIBCXX_3.4> 0x0000555555555588 <+26>: call 0x555555555050 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt> 0x000055555555558d <+31>: mov %rax,%rdx 0x0000555555555590 <+34>: mov 0x2a39(%rip),%rax # 0x555555557fd0 0x0000555555555597 <+41>: mov %rax,%rsi 0x000055555555559a <+44>: mov %rdx,%rdi 0x000055555555559d <+47>: call 0x555555555080 <_ZNSolsEPFRSoS_E@plt> 0x00005555555555a2 <+52>: nop 0x00005555555555a3 <+53>: leave 0x00005555555555a4 <+54>: ret End of assembler dump. (gdb) disassemble fun2 Dump of assembler code for function _ZN7Derived4fun2Ev: 0x00005555555555a6 <+0>: push %rbp 0x00005555555555a7 <+1>: mov %rsp,%rbp 0x00005555555555aa <+4>: sub $0x10,%rsp 0x00005555555555ae <+8>: mov %rdi,-0x8(%rbp) 0x00005555555555b2 <+12>: lea 0xa7a(%rip),%rsi # 0x555555556033 0x00005555555555b9 <+19>: lea 0x2ac0(%rip),%rdi # 0x555555558080 <_ZSt4cout@GLIBCXX_3.4> 0x00005555555555c0 <+26>: call 0x555555555050 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt> 0x00005555555555c5 <+31>: mov %rax,%rdx 0x00005555555555c8 <+34>: mov 0x2a01(%rip),%rax # 0x555555557fd0 0x00005555555555cf <+41>: mov %rax,%rsi 0x00005555555555d2 <+44>: mov %rdx,%rdi 0x00005555555555d5 <+47>: call 0x555555555080 <_ZNSolsEPFRSoS_E@plt> 0x00005555555555da <+52>: nop 0x00005555555555db <+53>: leave 0x00005555555555dc <+54>: ret End of assembler dump.