深入浅出C++虚函数的vptr与vtable

1.基础理论

为了实现虚函数,C ++使用一种称为虚拟表的特殊形式的后期绑定。该虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表。虚拟表有时会使用其他名称,例如“vtable”,“虚函数表”,“虚方法表”或“调度表”。

虚拟表实际上非常简单,虽然用文字描述有点复杂。首先,每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表。该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的最派生函数。

其次,编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针。

因此,它使每个类对象的分配大一个指针的大小。这也意味着vptr由派生类继承,这很重要。

2.实现与内部结构

下面我们来看自动与手动操纵vptr来获取地址与调用虚函数!

开始看代码之前,为了方便大家理解,这里给出调用图:

base

代码全部遵循标准的注释风格,相信大家看了就会明白,不明白的话,可以留言!

  1. /**
  2. * @file vptr1.cpp
  3. * @brief C++虚函数vptr和vtable
  4. * 编译:g++ -g -o vptr vptr1.cpp -std=c++11
  5. * @author 光城
  6. * @version v1
  7. * @date 2019-07-20
  8. */
  9. #include <iostream>
  10. #include <stdio.h>
  11. using namespace std;
  12. /**
  13. * @brief 函数指针
  14. */
  15. typedef void (*Fun)();
  16. /**
  17. * @brief 基类
  18. */
  19. class Base
  20. {
  21. public:
  22. Base(){};
  23. virtual void fun1()
  24. {
  25. cout << "Base::fun1()" << endl;
  26. }
  27. virtual void fun2()
  28. {
  29. cout << "Base::fun2()" << endl;
  30. }
  31. virtual void fun3(){}
  32. ~Base(){};
  33. };
  34. /**
  35. * @brief 派生类
  36. */
  37. class Derived: public Base
  38. {
  39. public:
  40. Derived(){};
  41. void fun1()
  42. {
  43. cout << "Derived::fun1()" << endl;
  44. }
  45. void fun2()
  46. {
  47. cout << "DerivedClass::fun2()" << endl;
  48. }
  49. ~Derived(){};
  50. };
  51. /**
  52. * @brief 获取vptr地址与func地址,vptr指向的是一块内存,这块内存存放的是虚函数地址,这块内存就是我们所说的虚表
  53. *
  54. * @param obj
  55. * @param offset
  56. *
  57. * @return
  58. */
  59. Fun getAddr(void* obj,unsigned int offset)
  60. {
  61. cout<<"======================="<<endl;
  62. void* vptr_addr = (void *)*(unsigned long *)obj; //64位操作系统,占8字节,通过*(unsigned long *)obj取出前8字节,即vptr指针
  63. printf("vptr_addr:%p\n",vptr_addr);
  64. /**
  65. * @brief 通过vptr指针访问virtual table,因为虚表中每个元素(虚函数指针)在64位编译器下是8个字节,因此通过*(unsigned long *)vptr_addr取出前8字节,
  66. * 后面加上偏移量就是每个函数的地址!
  67. */
  68. void* func_addr = (void *)*((unsigned long *)vptr_addr+offset);
  69. printf("func_addr:%p\n",func_addr);
  70. return (Fun)func_addr;
  71. }
  72. int main(void)
  73. {
  74. Base ptr;
  75. Derived d;
  76. Base *pt = new Derived(); // 基类指针指向派生类实例
  77. Base &pp = ptr; // 基类引用指向基类实例
  78. Base &p = d; // 基类引用指向派生类实例
  79. cout<<"基类对象直接调用"<<endl;
  80. ptr.fun1();
  81. cout<<"基类引用指向派生类实例"<<endl;
  82. pp.fun1();
  83. cout<<"基类指针指向派生类实例并调用虚函数"<<endl;
  84. pt->fun1();
  85. cout<<"基类引用指向基类实例并调用虚函数"<<endl;
  86. p.fun1();
  87. // 手动查找vptr 和 vtable
  88. Fun f1 = getAddr(pt, 0);
  89. (*f1)();
  90. Fun f2 = getAddr(pt, 1);
  91. (*f2)();
  92. delete pt;
  93. return 0;
  94. }

运行结果:

  1. 基类对象直接调用
  2. Base::fun1()
  3. 基类引用指向派生类实例
  4. Base::fun1()
  5. 基类指针指向派生类实例并调用虚函数
  6. Derived::fun1()
  7. 基类引用指向基类实例并调用虚函数
  8. Derived::fun1()
  9. =======================
  10. vptr_addr:0x401130
  11. func_addr:0x400ea8
  12. Derived::fun1()
  13. =======================
  14. vptr_addr:0x401130
  15. func_addr:0x400ed4
  16. DerivedClass::fun2()

我们发现C++的动态多态性是通过虚函数来实现的。简单的说,通过virtual函数,指向子类的基类指针可以调用子类的函数。例如,上述通过基类指针指向派生类实例,并调用虚函数,将上述代码简化为:

  1. Base *pt = new Derived(); // 基类指针指向派生类实例
  2. cout<<"基类指针指向派生类实例并调用虚函数"<<endl;
  3. 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. (gdb) b Derived::fun1
  2. Breakpoint 1 at 0x400eb4: file vptr1.cpp, line 23.
  3. (gdb) r
  4. Starting program: /home/light/Program/CPlusPlusThings/virtual/pure_virtualAndabstract_class/vptr
  5. 基类对象直接调用
  6. Base::fun1()
  7. 基类引用指向派生类实例
  8. Base::fun1()
  9. 基类指针指向派生类实例并调用虚函数
  10. Breakpoint 1, Derived::fun1 (this=0x614c20) at vptr1.cpp:23
  11. 23 cout << "Derived::fun1()" << endl;
  12. (gdb) disassemble fun1
  13. Dump of assembler code for function Derived::fun1():
  14. 0x0000000000400ea8 <+0>: push %rbp
  15. 0x0000000000400ea9 <+1>: mov %rsp,%rbp
  16. 0x0000000000400eac <+4>: sub $0x10,%rsp
  17. 0x0000000000400eb0 <+8>: mov %rdi,-0x8(%rbp)
  18. => 0x0000000000400eb4 <+12>: mov $0x401013,%esi
  19. 0x0000000000400eb9 <+17>: mov $0x602100,%edi
  20. 0x0000000000400ebe <+22>: callq 0x4009d0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
  21. 0x0000000000400ec3 <+27>: mov $0x400a00,%esi
  22. 0x0000000000400ec8 <+32>: mov %rax,%rdi
  23. 0x0000000000400ecb <+35>: callq 0x4009f0 <_ZNSolsEPFRSoS_E@plt>
  24. 0x0000000000400ed0 <+40>: nop
  25. 0x0000000000400ed1 <+41>: leaveq
  26. 0x0000000000400ed2 <+42>: retq
  27. End of assembler dump.
  28. (gdb) disassemble fun2
  29. Dump of assembler code for function Derived::fun2():
  30. 0x0000000000400ed4 <+0>: push %rbp
  31. 0x0000000000400ed5 <+1>: mov %rsp,%rbp
  32. 0x0000000000400ed8 <+4>: sub $0x10,%rsp
  33. 0x0000000000400edc <+8>: mov %rdi,-0x8(%rbp)
  34. 0x0000000000400ee0 <+12>: mov $0x401023,%esi
  35. 0x0000000000400ee5 <+17>: mov $0x602100,%edi
  36. 0x0000000000400eea <+22>: callq 0x4009d0 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
  37. 0x0000000000400eef <+27>: mov $0x400a00,%esi
  38. 0x0000000000400ef4 <+32>: mov %rax,%rdi
  39. 0x0000000000400ef7 <+35>: callq 0x4009f0 <_ZNSolsEPFRSoS_E@plt>
  40. 0x0000000000400efc <+40>: nop
  41. 0x0000000000400efd <+41>: leaveq
  42. 0x0000000000400efe <+42>: retq
  43. End of assembler dump.