写在前面
前面分析了 C++类内的虚指针和虚表, 通过二级指针解引用的方式找到虚表, 由此访问虚函数, 相较于传统的死记硬背, 我一直觉得学习编程时候能看到具体的/确切的输入输出结果, 对于掌握某个知识点要更加有效, 如果你只是知道了虚函数的原理, 却又说不清楚虚函数是怎样寻址的, 即其在类内具体存放的位置, 那么还是不能知其全貌, 掌握全局的.
下面的内容基于前一篇文章的分析, 进一步探索 C++类的多继承情况和虚继承情况下虚表/虚指针/虚函数的具体位置, 以及对象模型的一些分布情况(通过成员指针), 这里只针对 g++/clang++编译器, 所以可能有些片面, 不过像这种编译器实现应该大同小异的(因为都是 ISO 出来的)
本文内容部分参考了<深度探索 C++对象模型>, 里面的几张图给出了 C++在多继承和虚继承时候虚函数以及虚表的分布情况, 对于想要了解 C++对象模型的初学者有重要意义.
代码可以看我的 GitHub: Learn_C_CPP/oop_ood/virtual_func at master · zorchp/Learn_C_CPP; read 开头的几个文件就是本文的示例代码.
前置知识
一些约定
- 类内的函数为方便都采用以下签名:
using FUNC = void (*)();
- 指针变量的类型视平台而定:
// 指针变量的大小 #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 虚表中就有两份一样的虚函数实例, 可以用之前写的虚函数提取工具解析出来
总结
- 对于继承情况, 继承了几个基类, 派生类中就有几个虚表, 对应的实例对象中就有几根虚指针(用以分别指向虚表)
- 如果派生类在继承体系中定义了自己的虚函数, 该虚函数会注册到第一张虚表的后边
- 虚继承情况, 主要影响虚基类(间接基类/最终基类)在派生类(最终派生类)中保存了几份实例.