C++虚函数剖析 继承,多继承和虚继承情况

 
Category: C++

写在前面

前面分析了 C++类内的虚指针和虚表, 通过二级指针解引用的方式找到虚表, 由此访问虚函数, 相较于传统的死记硬背, 我一直觉得学习编程时候能看到具体的/确切的输入输出结果, 对于掌握某个知识点要更加有效, 如果你只是知道了虚函数的原理, 却又说不清楚虚函数是怎样寻址的, 即其在类内具体存放的位置, 那么还是不能知其全貌, 掌握全局的.

下面的内容基于前一篇文章的分析, 进一步探索 C++类的多继承情况和虚继承情况下虚表/虚指针/虚函数的具体位置, 以及对象模型的一些分布情况(通过成员指针), 这里只针对 g++/clang++编译器, 所以可能有些片面, 不过像这种编译器实现应该大同小异的(因为都是 ISO 出来的)

本文内容部分参考了<深度探索 C++对象模型>, 里面的几张图给出了 C++在多继承和虚继承时候虚函数以及虚表的分布情况, 对于想要了解 C++对象模型的初学者有重要意义.

代码可以看我的 GitHub: Learn_C_CPP/oop_ood/virtual_func at master · zorchp/Learn_C_CPP; read 开头的几个文件就是本文的示例代码.

前置知识

一些约定

  1. 类内的函数为方便都采用以下签名:
    using FUNC = void (*)();
    
  2. 指针变量的类型视平台而定:
    // 指针变量的大小
    #if __WORDSIZE == 64
    using TYPE = unsigned long long;
    #else
    using TYPE = int;
    #endif
    

工具函数

通过对象指针访问虚函数, 这里加上了两个参数, 一个是虚指针的偏移量(可能有多根虚指针), 一个是虚表的偏移量(虚表内的虚函数下标)

/* 仅针对类 C, 可视情况修改 */
FUNC getvfunc(C *pa, int vptr_offset = 0, int vtbl_offset = 0) {
    TYPE vptr = *(reinterpret_cast<TYPE *>(pa) + vptr_offset);
    FUNC vfunc = *(reinterpret_cast<FUNC *>(vptr) + vtbl_offset);
    return vfunc;
    // or C-style
    // return *((FUNC *)(*((TYPE *)pa + vptr_offset)) + vtbl_offset);
}

继承-简单情况

class B {
public:
    virtual void f1() { cout << "B::f1()\n"; }
    virtual void f2() { cout << "B::f2()\n"; }
};

class C : public B {
public:
    void f1() override final { cout << "C::f1()\n"; }
    virtual void f3() { cout << "C::f3()\n"; }
};
    // 均只含有个虚指针(即一张虚表)
    cout << sizeof(B) << endl; // 8
    cout << sizeof(C) << endl; // 8

虚指针当然是一个类含有一个了, 其中派生类 C 的虚表中还有基类中的虚函数

结果:

    auto pc = new C;
    getvfunc(pc, 0, 0)(); // C 重写的 f1
    getvfunc(pc, 0, 1)(); // 父类 B 的 f2
    getvfunc(pc, 0, 2)(); // C 自己的 f3
    // C::f1()
    // B::f2()
    // C::f3()

多继承情形

针对下面的派生类,

class B1 {
public:
    virtual void f1() { cout << "B1::f1()\n"; }
    virtual void fb1() { cout << "B1::fb1()\n"; }
};

class B2 {
public:
    virtual void f1() { cout << "B2::f1()\n"; }
};

class B3 {
public:
    virtual void f1() { cout << "B3::f1()\n"; }
};

class C : public B1, public B2, public B3 {
public:
    void f1() override final { cout << "C::f1()\n"; }
    virtual void f2() { cout << "C::f2()\n"; }
};

其包含三个基类, 基类都分别有一个虚函数, 所以基类大小都为 sizeof(TYPE),

    cout << sizeof(B1) << endl; // 8
    cout << sizeof(B2) << endl; // 8
    cout << sizeof(B3) << endl; // 8

对于派生类来说, 因为其自己还有一个不同的虚函数f2, 猜测其大小应该是4x8=32, 但是实际结果却是3x8=24,

    cout << sizeof(C) << endl; // 24

于是可以发现派生类内仅含有三根虚指针, 那派生类自己的虚函数f2去哪了呢? 用上面的工具分析:

void t3() {
    auto pc = new C;
    // 第一张虚表
    getvfunc(pc, 0, 0)();
    getvfunc(pc, 0, 1)();
    getvfunc(pc, 0, 2)();
    // C::f1()
    // B1::fb1()
    // C::f2()
    
    // 第二张虚表
    getvfunc(pc, 1, 0)();
    // C::f1()
    
    // 第三张虚表
    getvfunc(pc, 2, 0)();
    // C::f1()
}

说明类 C 自己的虚函数合并到第一根虚指针所指向的虚表中了

虚继承情形

这里的情况又要复杂一些了:

深度探索 C++对象模型书中提到, 最好不要在虚基类中加入数据成员, 否则分析起来会非常复杂

从上面的例子改动一下: (一般虚继承主要用在多派生类共同继承自同一基类的情况, 此时继承的基类前面声明virtual 可以保证最终派生类的基类部分仅初始化一份)

这里加上了宏开关, 之后用于判断间接基类产生数量时候会用到.

#define VIRTUAL_INHERIT

class B1 {
public:
    virtual void f1() { cout << "B1::f1()\n"; }
    virtual void fb1() { cout << "B1::fb1()\n"; }
    void ff() { cout << "B1::ff()\n"; }
};

class B2 : public
#ifdef VIRTUAL_INHERIT
           virtual
#endif
           B1 {
public:
    virtual void f1() override { cout << "B2::f1()\n"; }
    virtual void fb2() { cout << "B2::fb2()\n"; }
};

class B3 : public
#ifdef VIRTUAL_INHERIT
           virtual
#endif
           B1 {
public:
    virtual void f1() override { cout << "B3::f1()\n"; }
};

class C : public B2, public B3 {
public:
    void f1() override final { cout << "C::f1()\n"; }
    virtual void f2() { cout << "C::f2()\n"; }
};

输出一下 sizeof:

    cout << sizeof(B1) << endl;  // 8
    cout << sizeof(B2) << endl; // 8
    cout << sizeof(B3) << endl; // 8
    cout << sizeof(C) << endl;  // 16

可以发现虚表只有两张了, 就是基类的两张, 那么虚函数呢?

先来分析一下中间基类:

需要加一下工具:

  FUNC getvfunc(B2 *pa, int vptr_offset = 0, int vtbl_offset = 0) {
      TYPE vptr = *(reinterpret_cast<TYPE *>(pa) + vptr_offset);
      FUNC vfunc = *(reinterpret_cast<FUNC *>(vptr) + vtbl_offset);
      return vfunc;
  }
  
  FUNC getvfunc(B3 *pa, int vptr_offset = 0, int vtbl_offset = 0) {
      TYPE vptr = *(reinterpret_cast<TYPE *>(pa) + vptr_offset);
      FUNC vfunc = *(reinterpret_cast<FUNC *>(vptr) + vtbl_offset);
      return vfunc;
  }

情况如下:

void t31() {
    auto pb2 = new B2;
    getvfunc(pb2, 0, 0)();
    getvfunc(pb2, 0, 1)();
    getvfunc(pb2, 0, 2)();
    // B2::f1() 重写的
    // B1::fb1() 基类的
    // B2::fb2() 自己的
}

void t32() {
    auto pb3 = new B3;
    getvfunc(pb3, 0, 0)();
    getvfunc(pb3, 0, 1)();
    // B3::f1() 重写的
    // B1::fb1() 基类的
}

然后就是最终的派生类: 这里大家可以试试注释掉宏定义, 即不采用虚继承来跑, 看一下区别:

void t4() {
    // 针对派生类 C
    auto pc = new C;
    // 第一张虚表, 继承自 B2, 并且还有自己的虚函数
    getvfunc(pc, 0, 0)();
    getvfunc(pc, 0, 1)();
    getvfunc(pc, 0, 2)();
    getvfunc(pc, 0, 3)();
    // C::f1() 重写自B2
    // B1::fb1() B2 包含的 B1 的, 虚继承情形下仅有一份间接基类 B1 的虚函数
    // B2::fb2() B2 的
    // C::f2() 自己的
    cout << string(50, '=') << endl;
    // 第二张虚表, 继承自 B3
    getvfunc(pc, 1, 0)();
#ifndef VIRTUAL_INHERIT
    getvfunc(pc, 1, 1)();
#endif
    // C::f1() 重写自B3
    // B1::fb1() B3 中包含的 B1 的虚函数, 如果不进行虚继承, 类 C
    // 虚表中将会有两份
}

虚继承的作用就一目了然了, 如果不执行虚继承, C 虚表中就有两份一样的虚函数实例, 可以用之前写的虚函数提取工具解析出来

总结

  1. 对于继承情况, 继承了几个基类, 派生类中就有几个虚表, 对应的实例对象中就有几根虚指针(用以分别指向虚表)
  2. 如果派生类在继承体系中定义了自己的虚函数, 该虚函数会注册到第一张虚表的后边
  3. 虚继承情况, 主要影响虚基类(间接基类/最终基类)在派生类(最终派生类)中保存了几份实例.