虚函数表,以下简称虚表。
虚函数表指针,以下简称虚表指针。

虚函数原理

虚函数是(运行时)多态的基础,或者说多态就是通过虚函数实现。

废话一句,C++标准规定,在构造子类对象时先调用了父类的构造函数,在父类的构造函数中调用虚函数不会触发多态。

C++标准只定义了虚函数的概念,也就是多态的概念,具体的实现交由编译器去实现。大部分编译器就采用虚表+虚表指针的办法解决。

  1. class A {
  2. public:
  3. virtual void v_a(){}
  4. virtual ~A(){}
  5. int64_t _m_a;
  6. };
  7. int main(){
  8. A* a = new A();
  9. return 0;
  10. }

以下是上面这段程序的内存分布情况:
C  _虚函数 - 图1

  • 虚表
    • 如果一个类有虚函数(继承获得也可以),则它至少有一个虚表。虚表保存的是虚函数的指针,也就是函数指针的数组。虚表保存在只读数据区(静态区),也就是说,虚表在main函数执行之前就完成了初始化。
    • 综上,虚表是为了保存一个类的虚函数地址的。
  • 虚表指针
    • 指向虚表,在构造函数初始值列表中初始化,即早于构造函数体执行。也意味着构造函数不能是虚函数。
    • 有虚函数的类的对象有一个或多个虚表指针,对象通过这些指针即可获得所属类的虚函数。

简单说,对象调用虚函数时,通过遍历对象的虚表指针所指向的虚表,找到同名的虚函数地址然后调用。

上面只是说出了虚表的实现思路,编译器具体是如何实现的,就要通过代码实践去了解了。

虚表实现

VS2015。
实践代码:https://snippets.cacher.io/snippet/b85d0fd3b1f1f3c5e105
类继承结构:https://www.processon.com/view/link/6057a09fe401fd4c038a0012

  1. // **************************************************
  2. // 下面这段代码的工作:
  3. // 1、找到D类对象d的全部虚函数表指针,展示虚函数表指针子在对象中的分布情况。
  4. // 2、执行这些虚函数表的全部虚函数,展示每个虚函数表存储哪些虚函数。
  5. // **************************************************
  6. int main()
  7. {
  8. using FuncPtr = void( * )( ); // 成员函数指针
  9. // 类大小,每个类有自己的2个int成员。
  10. std::size_t size_ptr = sizeof( void * );
  11. std::size_t size_A = sizeof( A ); // 2个自己的变量 * sizeof(int) + 1个虚函数表 * sizeof(void*)
  12. std::size_t size_B = sizeof( B ); // 2个自己的变量 * sizeof(int) + 1个虚函数表 * sizeof(void*)
  13. std::size_t size_C = sizeof( C ); // 2个自己的变量 * sizeof(int) + 1个虚函数表 * sizeof(void*)
  14. std::size_t size_Top = size_A; // 最顶层类大小,2个自己的变量 * sizeof(int) + 1个虚函数表 * sizeof(void*)
  15. std::size_t size_AA = sizeof( AA ); // (2 * 1个(间接)父类 + 2个自己的变量) * sizeof(int) + 1个虚函数表指针 * sizeof(void*)
  16. std::size_t size_BB = sizeof( BB ); // (2 * 2个(间接)父类 + 2个自己的变量) * sizeof(int) + 2个虚函数表指针 * sizeof(void*)
  17. std::size_t size_D = sizeof( D ); // (2 * 5个(间接)父类 + 2个自己的变量) * sizeof(int) + 3个虚函数表指针 * sizeof(void*)
  18. // 从上面可以总结出,D类到底有几个虚函数表指针:
  19. // 如果D的(间接)父类有虚函数,则D的虚函数指针数量 =直接父类的虚函数指针数量的和。
  20. // 如果D的(间接)父类都没有虚函数,如果D有自己的虚函数,则D有一个虚函数指针;反之,则没有。
  21. // D有3个虚函数表指针 = 父类AA有1个虚函数表指针 + 父类BB有2个虚函数表指针
  22. uint64_t **vptr_A = nullptr; // 第1个虚函数表指针和D中A部分数据起始地址相同
  23. uint64_t **vptr_B = nullptr; // 第2个虚函数表指针和D中B部分数据起始地址相同
  24. uint64_t **vptr_C = nullptr; // 第3个虚函数表指针和D中C部分数据起始地址相同
  25. // 3个虚函数表指针就对应3个虚函数表,一个虚函数表就是一个函数指针的数组
  26. FuncPtr *vtable_A = nullptr; //
  27. FuncPtr *vtable_B = nullptr; //
  28. FuncPtr *vtable_C = nullptr; //
  29. D d;
  30. vptr_A = ( uint64_t ** ) &d; // 第1个虚函数表指针和D中A部分数据起始地址相同
  31. vptr_B = vptr_A + size_Top / size_ptr + size_Top / size_ptr - 1; // 第2个虚函数表指针和D中B部分数据起始地址相同
  32. vptr_C = vptr_B + size_B / size_ptr; // 第3个虚函数表指针和D中C部分数据起始地址相同
  33. vtable_A = reinterpret_cast<FuncPtr *>( *vptr_A );
  34. vtable_B = reinterpret_cast<FuncPtr *>( *vptr_B );
  35. vtable_C = reinterpret_cast<FuncPtr *>( *vptr_C );
  36. // 继承路径是D -> AA -> A,包含了这条路径上的所有虚函数
  37. ( *( vtable_A + 0 ) )( ); // 虚函数声明:A :: A_func1(),最终调用的AA:: A_func1(),AA覆盖了
  38. ( *( vtable_A + 1 ) )( ); // 虚函数声明:A :: A_func2(),最终调用的D :: A_func2(),D覆盖了
  39. ( *( vtable_A + 2 ) )( ); // 虚函数声明:AA::AA_func1(),最终调用的AA::AA_func1()
  40. ( *( vtable_A + 3 ) )( ); // 虚函数声明:AA::AA_func2(),最终调用的D ::AA_func2(),D覆盖了
  41. ( *( vtable_A + 4 ) )( ); // 虚函数声明:D :: D_func1(),最终调用的D :: D_func1()
  42. ( *( vtable_A + 5 ) )( ); // 虚函数声明:D :: D_func2(),最终调用的D :: D_func2()
  43. cout << endl;
  44. // 继承路径是D -> BB -> B,但是D的虚函数已经在第一个虚函数表中了,因此不包含D的虚函数
  45. ( *( vtable_B + 0 ) )( ); // 虚函数声明:B :: B_func1(),最终调用的BB:: B_func1(),BB覆盖了
  46. ( *( vtable_B + 1 ) )( ); // 虚函数声明:B :: B_func2(),最终调用的D :: B_func2(),D覆盖了
  47. ( *( vtable_B + 2 ) )( ); // 虚函数声明:BB::BB_func1(),最终调用的BB::BB_func1()
  48. ( *( vtable_B + 3 ) )( ); // 虚函数声明:BB::BB_func2(),最终调用的D ::BB_func2(),D覆盖了
  49. cout << endl;
  50. // 继承路径是D -> BB -> C,只有C中的虚函数没有在其他虚函数表中。
  51. ( *( vtable_C + 0 ) )( ); // 虚函数声明:C::C_func1(),最终调用的C::C_func1()
  52. ( *( vtable_C + 1 ) )( ); // 虚函数声明:C::C_func2(),最终调用的C::C_func2()
  53. // 从上可总结出,每个虚函数表中的虚函数有哪些:
  54. // 第一个虚表,保存了这条继承路径上,所经过的类的所有虚函数。
  55. // 非第一个虚函数表,同上,且除了前面虚函数表中所包含过的虚函数。
  56. return 0;
  57. }

实践总结

点击查看【processon】

虚表

  • 编译器会为每个有虚函数的类在只读数据区创建1个或多个虚函数表(函数指针数组),用于存储该类的虚函数指针。
  • 类有几个虚表
    • 如果类的(间接)父类有虚函数,则虚表数量 = 直接父类的虚表之和。
    • 如果类的(间接)父类没有虚函数,如果类有自己的虚函数,则有1个虚表,反之,则没有虚表。
    • 简单理解
      • 有红绿蓝三条互不包容的继承链,因此有三个虚表。
  • 存储哪些虚函数?

    • 每个虚表保存对应继承链路上的所有类的所有虚函数,除了已经被前面的虚表所包含的虚函数。
      • 如上面的红色、绿色、蓝色部分。分别是三个虚表所包含的虚函数。

        虚表指针

  • 一个虚表指针指向一个虚表

    • 一个类有n个虚表,则类的对象有n个虚表指针。
  • 虚表指针的地址
    • 对象的虚表指针到底在对象内部的哪个位置,从对象起始偏移几个字节。
    • 和对应继承链上,有虚表的最顶层类的对象的地址相同。
      • 如上图,分别和D中A、B、C部分数据的起始地址相同。
  • 虚表指针顺序
    • 对象可能有多个虚表指针,到底哪个在第一个?
    • 按照派生列表的顺序排。
      • 如上图,依次是A、B、C

        多态过程

        当以指针、引用形式调用虚函数时,触发多态。 ```cpp

struct Base { virtual void func() {} }; struct Derived : Base { void func() {} };

Derived d; Base* p = d; p->func(); //当用指针、引用形式调用虚函数时,触发触发多态。 //多态过程: //1、获取p

``` p->func()过程如下:

  • 获取虚表:
    • 从上面的学习,我们知道,d对象首位置存储的是指向Derived类虚表的指针,我们直接对d对象地址进行解引用即可获得指针。
  • 查找函数
    • 在虚表中查找匹配的函数的地址,根绝我们上面的试验得知,在“第一行”的虚表中就能命中函数。
  • 调用虚函数。

    多态代价

    1、每个对象需要一个额外的指针保存虚表地址。
    2、每次触发多态,就需要在虚表中查找函数,这比非多态调用要慢一些。

    不得不提

    前面我们提到虚表指针并不是指向虚表的起始地址,而是起始地址偏移了若干字节后的位置。其实在这里面存储了一个指向type_info数据的指针,typeid函数就是通过读取这个地址来在运行时获得对象的类型信息。
    而type_info数据则存储在内存的可读可写data区,代码中只要有用到typeid的类型,编译器都会为该类型保存一份type_info数据。