signed

QiShunwang

“诚信为本、客户至上”

C++虚函数实现详细解析 (附示例)

2021/5/15 0:48:12   来源:

目录

          • 前言
            • 运行环境
          • 什么是虚函数
            • 简单示例
          • 虚函数的实现原理
            • 虚函数表
            • 通过虚表找到函数地址并调用
          • 虚函数表的具体分析
            • 无继承关系时的虚函数表
            • 单继承但未重写虚函数时的虚函数表
            • 单继承且重写虚函数时的虚函数表
            • 多继承但未重写虚函数时的虚函数表
            • 多继承且重写虚函数时的虚函数表
          • 奇技淫巧
            • 通过父类指针访问子类的虚函数
            • 访问non-public的虚函数
          • 额外的思考
          • 结束语
          • 完整代码
          • 参考资料

前言

\quad\quad 相信很多人在了解到多态之后,都想要知道其具体的实现原理。其实大家应该都或多或少地知道:虚函数是通过虚函数表实现的。但是呢,可能和我之前一样,知道大概是怎样的,但是没有通过代码真正地运行测试。最近我也在网上查看了一番,发现有的文章写得很好,但是代码上面可能欠考虑一点,有的则是没有详细解释。因此本文将使用详细示例来探讨虚函数的实现原理,教你如何通过对象查找虚函数地址并调用。末尾还会分享一些没有用但好玩的技巧。

运行环境

本文代码在以下环境编译通过,且运行结果一致。如果你们的运行结果不一致,欢迎评论反馈。

  • VS2017、C++11
  • g++ (GCC) 4.8.5、C++11
什么是虚函数

\quad\quad 虚函数是可在派生类中覆盖其行为的成员函数。与非虚函数相反,即使没有关于该类实际类型的编译时信息,仍然保留进行覆盖的行为。当使用到基类的指针或引用来处理派生类时,对被覆盖的虚函数的调用,将会调用定义于派生类中的行为。这种函数调用被称为虚函数调用或虚调用。当使用有限定名字查找(即函数名出现在作用域解析运算符 :: 的右侧)时,虚函数调用被抑制。

\quad\quad 相信看这篇文章的你一定是知道虚函数的作用,这里也就不详细说了。虚函数的详细介绍见virtual 函数说明符。

简单示例
#include <iostream>
struct Base {
   virtual void f() {
       std::cout << "base\n";
   }
};
struct Derived : Base {
    void f() override { // 'override' 可选
        std::cout << "derived\n";
    }
};
int main()
{
    Base b;
    Derived d;
 
    // 通过引用调用虚函数
    Base& br = b; // br 的类型是 Base&
    Base& dr = d; // dr 的类型也是 Base&
    br.f(); // 打印 "base"
    dr.f(); // 打印 "derived"
 
    // 通过指针调用虚函数
    Base* bp = &b; // bp 的类型是 Base*
    Base* dp = &d; // dp 的类型也是 Base*
    bp->f(); // 打印 "base"
    dp->f(); // 打印 "derived"
 
    // 非虚函数调用
    br.Base::f(); // 打印 "base"
    dr.Base::f(); // 打印 "base"
}
虚函数的实现原理
虚函数表

\quad\quad 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表是类共享的,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
\quad\quad C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。

通过虚表找到函数地址并调用

我们现在有这样一个类:

class Base
{
public:
  virtual void f() { std::cout << "Base::f" << std::endl; }
  virtual void g() { std::cout << "Base::g" << std::endl; }
  virtual void h() { std::cout << "Base::h" << std::endl; }
};

那么他的虚表和对象关系如下所示:
在这里插入图片描述
\quad\quad 当然在本例中Base没有其他成员变量,我画出来是为了方便理解。实际上sizeof(Base)得到的就是一个指针的大小,因为Base没有其他成员,只有虚函数,因此只需要存一个虚函数表的地址即可。注意:虚函数表是类共享的,不是每个类的实例都有一个虚函数表,而是每个类的实例都存有虚函数表的起始地址。
\quad\quad 那么现在我们知道虚函数如何存储了,那么指针学得较好的同学可能就会想:那我是不是可以直接从对象里手动去找到虚函数的函数地址并调用呢?答案是肯定的。只需要如下几步:

  • 得到虚函数表的地址:我们知道虚函数表的地址是在对象所占空间的最前面。因此可以这样:
    Base base;
    Base *pb = &base;
    // 把base空间的前4个字节按int读出来,由于我们知道它是指针,因此我们直接转成指针类型并打印
    // 但是这里有个问题:指针在32位是4字节,但是64位程序是8字节,因此这样的代码是不通用的,
    // 后面会使用通用的代码进行解析。
    void **v_table_ptr = (void **)(*(int *)pb);
    
  • 找到虚函数f的地址并调用:我们知道了虚表的地址实际上虚表里的元素就是虚函数的地址。因此可以这样:
    // 定义函数指针vf,获取虚表的第一个元素,我们知道它是Base的虚函数f的地址,
    // 因此直接转为f的函数指针类型:void (*)()
    void (*vf)() = (void (*)())(v_table_ptr[0]);
    vf();
    
    这样就会打印出Base::f()
    当然你可能会觉得上面的代码很绕,那么我就给出一个简化的版本,让你能轻松理解这几行代码,也是为后面的示例做准备。
    1. 首先我们定义一个类型别名:using func_type = void (*)()或者typedef void (*func_type)();这两种方式都是可以的,但是我推荐用C++的using来定义,因为我们很明显可以看出用using可读性更高。
    2. 然后呢,我们知道虚函数表其实就是一个数组,里面存了函数地址,因此虚函数表就是一个指针数组
      那么我们就可以定义一个基本指针类型pointer:using pointer = void *;
      然后定义一个指针数组类型:using pointer_arr = pointer *;
    3. 现在我们考虑如何取到虚表地址:pointer_arr v_table_ptr = *(pointer_arr *)(pb);,因为pb的前几个字节存的是pointer_arr的地址,因此我们将其转为pointer_arr *类型,再解引用,就得到了pointer_arr的地址。
    4. 这下找到函数地址并调用就简单了:
      func_type vf = (func_type)(v_table_ptr[0]);
      func_type vg = (func_type)(v_table_ptr[1]);
      vf();
      vg();
      
      这样通过类型别名,是不是这几步操作就十分通俗易懂了呢?所以对于复杂类型,有时善用类型重定义,有化繁为简的效果。
    5. 完整代码:
      #include <iostream>
      
      class Base
      {
        virtual void f() { std::cout << "Base::f" << std::endl; }
        virtual void g() { std::cout << "Base::g" << std::endl; }
        virtual void h() { std::cout << "Base::h" << std::endl; }
      };
      
      int main()
      {
        using func_type = void(*)();
        using pointer = void *;
        using pointer_arr = pointer * ;
      
        Base base;
        Base *pb = &base;
      
        // 使用这种方式访问还有一个好处就是32位和64位兼容,
        // 上面使用int *去取一个int的字节,在64位下就是错误的。
        // 而我们这里是取指针,而指针在32位下4字节,64位下8字节,
        // 因此是兼容的写法,可读性也更高
        pointer_arr v_table_ptr = *(pointer_arr *)(pb);
        std::cout << "虚函数表地址是: " << v_table_ptr << std::endl;
        func_type vf = (func_type)(v_table_ptr[0]);
        func_type vg = (func_type)(v_table_ptr[1]);
        func_type vh = (func_type)(v_table_ptr[2]);
      
        vf();
        vg();
        vh();
      
        return 0;
      }
      
虚函数表的具体分析

在具体分析之前,我们需要做一点工作,来让后面的代码更简洁也更清晰

  • 定义一个宏函数VFuncDefine:我们用它来简化我们virtual函数的定义。
    #define VFuncDefine(_class, name) \
      virtual void name() { std::cout<<#_class"::"#name<<std::endl; } \
    
    // VFuncDefine(Base,f) =>
    // virtual void f()
    // {
    //   std::cout << "Base::f" << std::endl;
    // }
    

这样我们可以给出Base类的定义如下:

class Base
{
public:
  VFuncDefine(Base, f)
  VFuncDefine(Base, g)
  VFuncDefine(Base, h)
};
// 完全一样的写法,看起来也没有简洁,只是我想偷懒少写几个字母而已哈哈
class Base
{
public:
  virtual void f() { std::cout << "Base::f" << std::endl; }
  virtual void g() { std::cout << "Base::g" << std::endl; }
  virtual void h() { std::cout << "Base::h" << std::endl; }
};

这样我们就开始分情况讨论虚函数表的几种情况了。

无继承关系时的虚函数表

无继承关系时的虚函数表其实你已经见过了就是这个:

  • 虚函数按照其声明顺序放于表中。
    在这里插入图片描述
    测试代码如下:
class Base
{
public:
  VFuncDefine(Base, f)
  VFuncDefine(Base, g)
  VFuncDefine(Base, h)
};
void test1()
{
  Base base;
  Base *pb = &base;
  pointer_arr v_table_ptr = *(pointer_arr*)(pb);
  auto f = func_type(v_table_ptr[0]);
  auto g = func_type(v_table_ptr[1]);
  auto h = func_type(v_table_ptr[2]);

  f(); // 打印 Base::f
  g(); // 打印 Base::g
  h(); // 打印 Base::h
}
单继承但未重写虚函数时的虚函数表

最简单的无继承情况下的虚函数表我们知道是怎样的了,那么考虑下面class A的虚函数表是怎样的?

class A : public Base
{
public:
  VFuncDefine(A, f1)
  VFuncDefine(A, g1)
  VFuncDefine(A, h1)
}; 

答案是:

  • 继承的类会派生一个虚函数表,如果存在虚函数的话,并copy该父类的虚函数地址。
  • 虚函数按照其声明顺序放于表中。
  • 父类的虚函数在子类的虚函数前面。

如图所示:
在这里插入图片描述
注意:这里的虚函数表地址是A的虚函数表地址,它继承了Base的虚函数表,并且添加自己的虚函数在后面,这是个新的表。换句话说:A的虚表地址和Base的虚表地址不等,即不是同一张表。其实很正常,因为如果相同的话,那么Base类就会被子类影响了,肯定是不合理的。

测试代码如下:

void test2()
{
  A a;
  A *pa = &a;
  pointer_arr v_table_ptr = *(pointer_arr*)(pa);
  for (int i = 0; i < 6; ++i) {
    auto func = (func_type)(v_table_ptr[i]);
    func();
  }
  /* 结果
  	Base::f
	Base::g
	Base::h
	A::f1
	A::g1
	A::h1
  */
}
单继承且重写虚函数时的虚函数表

上面那种是没有重写的情况,我们在class A的基础上稍加改动得到class B

class B : public Base
{
public:
  VFuncDefine(B, f) // 重写了虚函数f,因为这里重名了且类型相同
  VFuncDefine(B, g1)
  VFuncDefine(B, h1)
};

这种情况下,如果想实现多态你猜猜应该怎么做呢?没错,就是覆盖父类对应的虚函数地址。
即:

  • 继承的类会派生一个虚函数表,如果存在虚函数的话,并copy该父类的虚函数地址。
  • override的函数被放到了新虚表中父类虚函数地址所在位置,即父类虚函数地址被覆盖,从而实现多态。
  • 没有被覆盖的函数则依旧按照声明顺序排列在虚表后面。

如下图:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210514205927379.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM2MjM5NTY5,size_16,color_FFFFFF,t_70

测试代码如下:

void test3()
{
  B b;
  B *pb = &b;
  pointer_arr v_table_ptr = *(pointer_arr*)(pb);
  for (int i = 0; i < 5; ++i) {
    auto func = (func_type)(v_table_ptr[i]);
    func();
  }
  /* 结果
    B::f
	Base::g
	Base::h
	B::g1
	B::h1
  */
}
多继承但未重写虚函数时的虚函数表

考虑了单继承,现在思考多继承情况下应该怎么实现?考虑如下类:

class Base2
{
public:
  VFuncDefine(Base2, f)
  VFuncDefine(Base2, g)
  VFuncDefine(Base2, h)
};
// Base的定义上面有,这里省略了
class C : public Base, public Base2
{
public: // 未重写虚函数
  VFuncDefine(C, f1)
  VFuncDefine(C, g1)
  VFuncDefine(C, h1)
};

多继承时,子类会有多个虚函数表,其子类对象就会存放多个虚函数表的地址。
即:

  • 每个继承的类都会派生一个虚函数表,如果存在虚函数的话,并copy对应父类的虚函数地址。
  • 子类的虚函数被放到了第一个虚表中。(所谓的第一个虚表是按照父类声明顺序来判断的)

如下图:
在这里插入图片描述
注意:这个地方其实有个知识点可以讲,那就是关于子类转基类指针的偏移问题。
举个例子:有时候我们会遇到一些需要使用void *作为指针传参的中转。

void test4_2()
{
    C c;
    C *pc = &c;

    Base2 *real_pb = pc;
    void *tmp_ptr = pc;
    Base2 *error_pb = reinterpret_cast<Base2 *>(tmp_ptr);

    std::cout << "pc = " << pc << std::endl;
    std::cout << "real_pb = " << real_pb << std::endl;
    std::cout << "error_pb = " << error_pb << std::endl;

    /*  结果
        pc = 0x7ffd67609190
        real_pb = 0x7ffd67609198
        error_pb = 0x7ffd67609190
    */
}

解析:注意这里的error_pb转换是错的,是因为pc转为void *后丢失了类型,再转Base2*,就不会调整指针位置,从而导致指向错误的虚函数表地址。如果用error_pb去遍历虚函数表,就会得到Base的虚函数表地址,因为Base是第一个父类,所以占了前面一个指针的空间用来存放虚函数表,因此Base2的虚函数表是需要往后偏移的,如果丢失类型,编译器就不会帮你偏移,从而指向错误的地址。

本例完整测试代码:

void test4_2();

void test4()
{
    C c;
    C *pc = &c;
    {
        // 多继承按继承顺序,有多个虚函数表,子类的虚函数放到第一个父类虚函数的后面
        pointer_arr v_table_ptr = *(pointer_arr *)(pc);
        for (int i = 0; i < 6; ++i)
        {
            auto func = (func_type)(v_table_ptr[i]);
            func();
        }
    }

    {
        // 这是Base2的虚函数表
        pointer_arr v_table_ptr = *((pointer_arr *)(pc) + 1);
        for (int i = 0; i < 3; ++i)
        {
            auto func = (func_type)(v_table_ptr[i]);
            func();
        }
    }

    test4_2();
	/* 结果
		Base::f
		Base::g
		Base::h
		C::f1
		C::g1
		C::h1
		Base2::f
		Base2::g
		Base2::h
		pc = 0x7ffdffcabb30
		real_pb = 0x7ffdffcabb38
		error_pb = 0x7ffdffcabb30
	*/
}

void test4_2()
{
    C c;
    C *pc = &c;

    Base2 *real_pb = pc;
    void *tmp_ptr = pc;
    Base2 *error_pb = reinterpret_cast<Base2 *>(tmp_ptr);

    std::cout << "pc = " << pc << std::endl;
    std::cout << "real_pb = " << real_pb << std::endl;
    std::cout << "error_pb = " << error_pb << std::endl;

    /*  结果
		pc = 0x7ffdffcabb30
		real_pb = 0x7ffdffcabb38
		error_pb = 0x7ffdffcabb30
    */
}
多继承且重写虚函数时的虚函数表

终于说到最后一种情况,这种情况的实现相信你结合上面的几种情况是可以推出来的。还是老规矩,考虑如下类:

class D : public Base, public Base2
{
public:
    VFuncDefine(D, f) // 重写虚函数f
    VFuncDefine(D, g1)
    VFuncDefine(D, h1)
};

它的虚函数表实现是:

  • 每个继承的类都会派生一个虚函数表,如果存在虚函数的话,并copy对应父类的虚函数地址。
  • override的函数被放到了新虚表中父类虚函数地址所在位置,即父类虚函数地址被覆盖,从而实现多态。
  • 子类没重写父类虚函数的虚函数被放到了第一个虚表中。(所谓的第一个虚表是按照父类声明顺序来判断的)

如图:
在这里插入图片描述
测试代码如下:

void test5()
{
    D d;
    D *pd = &d;
    {
        // 多继承按继承顺序,有多个虚函数表,子类的虚函数放到第一个父类虚函数的后面
        pointer_arr v_table_ptr = *(pointer_arr *)(pd);
        for (int i = 0; i < 5; ++i)
        {
            auto func = (func_type)(v_table_ptr[i]);
            func();
        }
    }

    {
        Base2 *pb = pd;
        pointer_arr v_table_ptr = *(pointer_arr *)(pb);
        for (int i = 0; i < 3; ++i)
        {
            auto func = (func_type)(v_table_ptr[i]);
            func();
        }
    }

    /* 结果
        D::f
        Base::g
        Base::h
        D::g1
        D::h1
        D::f
        Base2::g
        Base2::h
    */
}
奇技淫巧
通过父类指针访问子类的虚函数
void test6()
{
    C c;
    Base *p_base = &c;
    auto c_f1 = (func_type)(*(pointer_arr *)(p_base))[3];
    // 直接通过p_base->f1();编译器是不会让你通过的
    // 但是可以通过虚函数表进行访问,前提是你知道这个指针就是子类指针
    c_f1(); // 打印 C::f1

    Base2 *p_base2 = &c; 
    // 这里的前提是我们知道这个指针是子类的,并且Base2是其第二个父类,
    // 因此我们可以直接将指针向低地址偏移一个指针的大小,这样就到了对象c的首地址处
    // 就可以通过虚函数表找到c的虚函数
    auto base_f = (func_type)(*(pointer_arr *)((void **)p_base2 - 1))[0];
    auto c_g1 = (func_type)(*(pointer_arr *)((void **)p_base2 - 1))[4];

    base_f(); // 打印 Base::f
    c_g1(); // 打印 C::g1
}
访问non-public的虚函数
class E
{
private:
    VFuncDefine(E, f)
    VFuncDefine(E, g)
    VFuncDefine(E, h)
};

void test7()
{
    E e;
    E *pe = &e;

    // 无法访问pe->f();因为是private的,但它是虚函数,因此我们可以通过虚函数表访问
    pointer_arr v_table_ptr = *(pointer_arr *)(pe);
    for (int i = 0; i < 3; ++i)
    {
        auto func = (func_type)(v_table_ptr[i]);
        func();
    }

    /* 结果
        E::f
        E::g
        E::h
    */
}
额外的思考

如果你认真运行了代码,也理解了,那我提个问:

  • 按照我们现在这种找虚函数地址再调用的方式,如果虚函数里用到了类成员变量会怎样呢?

答案是运行会出错。因为使用类成员变量那必然需要this指针,但很明显我们的方式是没有传this的,那函数不知道我们的this,那肯定就会出错,会访问到错误的地址。实际上这个问题应该是类成员函数是怎样实现的?这可就说来话长了,详细讲的话那也是需要这样一篇文章才能讲得清楚。不过可以简单地说一下:

  • thunk技术:将一段机器码对应的字节保存在一个连续内存结构里,
    然后将其指针强制转换成函数. 即用作函数来执行。
  • 编译器使用thunk技术来解决类成员函数的this指针传递问题,要知道this指针的传递可不简单,因为还需要支持多态,有时this是需要偏移的。比如我们上面提到过的多继承的情况下,
    class Base3 
    {
    public:
        VFuncDefine(Base3, hello);
    };
    
    class F : public Base, public Base3
    {
    
    };
    
    void test8() 
    {
        F f;
        F *pf = &f;
    
    	pf->f();
        pf->hello();
    }
    
    这里调用hello可以用来解释为何需要thunk技术,因为前面讲了,Base3是第二个继承的类,那么虚表地址存放在对象地址偏移一个指针大小的后面的地方,因此真正传递个函数的指针是需要经过偏移计算的,而我们调用都是pf->f()pf->hello(),我们没有做偏移,那么就需要编译器来做这个事了。

后续可能会再研究下,写一篇关于类成员函数的实现分析的文章。先挖个坑。

结束语

\quad\quad C++的东西真的太多了,学习真是永无止境。
\quad\quad 那么虚函数的具体实现分析到这里就告一段落了,欢迎在评论区指正我的错误之处。另外如果有什么疑问的话,也可以留言交流。如果觉得写得还不错,就点个赞再走吧!

完整代码
  • 注意:编译需要编译器支持C++11,linux下使用g++编译
    g++ -std=c++11 -o virtual_test virtual_test.cpp
/* virtual_test.cpp */
#include <iostream>

#define VFuncDefine(_class, name) \
    virtual void name() { std::cout << #_class "::" #name << std::endl; }

// VFuncDefine(Base,f) =>
// virtual void f()
// {
//   std::cout << "Base::f" << std::endl;
// }

// 定义函数指针类型,因为这里我们定义的函数没有返回值和参数,因此可以不写调用约定
using func_type = void (*)();
using pointer = void *;
using pointer_arr = pointer *;

class Base
{
public:
    VFuncDefine(Base, f)

        VFuncDefine(Base, g)

            VFuncDefine(Base, h)
};

class A : public Base
{
public:
    VFuncDefine(A, f1)

        VFuncDefine(A, g1)

            VFuncDefine(A, h1)
};

class B : public Base
{
public:
    VFuncDefine(B, f)

        VFuncDefine(B, g1)

            VFuncDefine(B, h1)
};

class Base2
{
public:
    VFuncDefine(Base2, f)

        VFuncDefine(Base2, g)

            VFuncDefine(Base2, h)
};

class C : public Base, public Base2
{
public:
    VFuncDefine(C, f1)

        VFuncDefine(C, g1)

            VFuncDefine(C, h1)
};

class D : public Base, public Base2
{
public:
    VFuncDefine(D, f)

        VFuncDefine(D, g1)

            VFuncDefine(D, h1)
};

class E
{
private:
    VFuncDefine(E, f)
        VFuncDefine(E, g)
            VFuncDefine(E, h)
};

class Base3 
{
public:
    VFuncDefine(Base3, hello);
};

class F : public Base, public Base3
{

};

void test8() 
{
    F f;
    F *pf = &f;

    pf->f();
    pf->hello();
}

void test1()
{
    Base base;
    Base *pb = &base;
    pointer_arr v_table_ptr = *(pointer_arr *)(pb);
    auto f = func_type(v_table_ptr[0]);
    auto g = func_type(v_table_ptr[1]);
    auto h = func_type(v_table_ptr[2]);

    f();
    g();
    h();
}

void test2()
{
    A a;
    A *pa = &a;
    pointer_arr v_table_ptr = *(pointer_arr *)(pa);
    for (int i = 0; i < 6; ++i)
    {
        auto func = (func_type)(v_table_ptr[i]);
        func();
    }
}

void test3()
{
    B b;
    B *pb = &b;
    pointer_arr v_table_ptr = *(pointer_arr *)(pb);
    for (int i = 0; i < 5; ++i)
    {
        auto func = (func_type)(v_table_ptr[i]);
        func();
    }
}

void test4_2();

void test4()
{
    C c;
    C *pc = &c;
    {
        // 多继承按继承顺序,有多个虚函数表,子类的虚函数放到第一个父类虚函数的后面
        pointer_arr v_table_ptr = *(pointer_arr *)(pc);
        for (int i = 0; i < 6; ++i)
        {
            auto func = (func_type)(v_table_ptr[i]);
            func();
        }
    }

    {
        // 这是Base2的虚函数表
        pointer_arr v_table_ptr = *((pointer_arr *)(pc) + 1);
        for (int i = 0; i < 3; ++i)
        {
            auto func = (func_type)(v_table_ptr[i]);
            func();
        }
    }

    test4_2();
}

void test4_2()
{
    C c;
    C *pc = &c;

    Base2 *real_pb = pc;
    void *tmp_ptr = pc;
    Base2 *error_pb = reinterpret_cast<Base2 *>(tmp_ptr);

    std::cout << "pc = " << pc << std::endl;
    std::cout << "real_pb = " << real_pb << std::endl;
    std::cout << "error_pb = " << error_pb << std::endl;

    /*  结果
        pc = 0x7ffd67609190
        real_pb = 0x7ffd67609198
        error_pb = 0x7ffd67609190
    */
}

void test5()
{
    D d;
    D *pd = &d;
    {
        // 多继承按继承顺序,有多个虚函数表,子类的虚函数放到第一个父类虚函数的后面
        pointer_arr v_table_ptr = *(pointer_arr *)(pd);
        for (int i = 0; i < 5; ++i)
        {
            auto func = (func_type)(v_table_ptr[i]);
            func();
        }
    }

    {
        Base2 *pb = pd;
        pointer_arr v_table_ptr = *(pointer_arr *)(pb);
        for (int i = 0; i < 3; ++i)
        {
            auto func = (func_type)(v_table_ptr[i]);
            func();
        }
    }

    /* 结果
        D::f
        Base::g
        Base::h
        D::g1
        D::h1
        D::f
        Base2::g
        Base2::h
    */
}

void test6()
{
    C c;
    Base *p_base = &c;
    auto c_f1 = (func_type)(*(pointer_arr *)(p_base))[3];
    // 直接通过p_base->f1();编译器是不会让你通过的
    // 但是可以通过虚函数表进行访问,前提是你知道这个指针就是子类指针
    c_f1(); // 打印 C::f1

    Base2 *p_base2 = &c;
    // 这里的前提是我们知道这个指针是子类的,并且Base2是其第二个父类,
    // 因此我们可以直接将指针向低地址偏移一个指针的大小,这样就到了对象c的首地址处
    // 就可以通过虚函数表找到c的虚函数
    auto base_f = (func_type)(*(pointer_arr *)((void **)p_base2 - 1))[0];
    auto c_g1 = (func_type)(*(pointer_arr *)((void **)p_base2 - 1))[4];

    base_f(); // 打印 Base::f
    c_g1();   // 打印 C::g1
}

void test7()
{
    E e;
    E *pe = &e;

    // 无法访问pe->f();因为是private的,但它是虚函数,因此我们可以通过虚函数表访问
    pointer_arr v_table_ptr = *(pointer_arr *)(pe);
    for (int i = 0; i < 3; ++i)
    {
        auto func = (func_type)(v_table_ptr[i]);
        func();
    }

    /* 结果
        E::f
        E::g
        E::h
    */
}

int main()
{
#define Run(func)                                                                        \
    do                                                                                   \
    {                                                                                    \
        std::cout << "----------------- " #func " start -----------------" << std::endl; \
        func();                                                                          \
    } while (0)

    // 无继承时虚函数表
    // + `虚函数按照其声明顺序放于表中。`
    Run(test1);

    // 无覆盖情况下的单继承类虚函数表
    /*
    + `继承的类会派生一个虚函数表,如果存在虚函数的话,并copy该父类的虚函数地址。`
    + `虚函数按照其声明顺序放于表中。`
    + `父类的虚函数在子类的虚函数前面。`
    */
    Run(test2);

    // 有覆盖情况下的单继承类虚函数表
    /*
    + `继承的类会派生一个虚函数表,如果存在虚函数的话,并copy该父类的虚函数地址。`
    + `override的函数被放到了新虚表中父类虚函数地址所在位置,即父类虚函数地址被覆盖,从而实现多态。`
    + `没有被覆盖的函数则依旧按照声明顺序排列在虚表后面。`
    */
    Run(test3);

    // 无覆盖情况下的多继承类虚函数表
    /*
    + `每个继承的类都会派生一个虚函数表,如果存在虚函数的话,并copy对应父类的虚函数地址。`
    + `子类的虚函数被放到了第一个虚表中。(所谓的第一个虚表是按照父类声明顺序来判断的)`
    */
    Run(test4);

    // 有覆盖情况下的多继承类虚继承表
    /*
    + `每个继承的类都会派生一个虚函数表,如果存在虚函数的话,并copy对应父类的虚函数地址。`
    + `override的函数被放到了新虚表中父类虚函数地址所在位置,即父类虚函数地址被覆盖,从而实现多态。`
    + `子类没重写父类虚函数的虚函数被放到了第一个虚表中。(所谓的第一个虚表是按照父类声明顺序来判断的)`
    */
    Run(test5);

    // 通过基类指针访问子类虚函数
    Run(test6);

    // 访问non-public虚函数
    Run(test7);

    Run(test8);

    return 0;
}

参考资料
  • C++虚函数解析