C++虚函数表实现机制以及用C语言对其进行的模拟实现(加入了自己的思考和笔记)
文章目录
- 前言
- C++对象的内存布局
- 没有虚函数的对象
- 拥有仅一个虚函数的类对象
- 拥有多个虚函数的类对象
- 单继承且本身不存在虚函数的继承类的内存布局
- 本身不存在虚函数(不严谨)但存在基类虚函数覆盖的单继承类的内存布局
- 定义了基类没有的虚函数的单继承的类对象布局
- 多继承且存在虚函数覆盖同时又存在自身定义的虚函数的类对象布局
- d1 是 Derive1 类型的对象,为什么说它的地址就是 Base1 子对象的起始位置?
- 为什么是 edx + 0Ch(即 vtable[3])
- 看看现在的类对象布局图:
- 如果第1个直接基类没有虚函数(表)
- Derive1 对象实际内存布局
- What if 两个基类都没有虚函数表
- 如果有三个基类: 虚函数表分别是有, 没有, 有!
- 什么Derive1的偏移量是40而不是44?
- C++中父子对象指针间的转换与函数调用
- 用C语言完全模拟C++虚函数表的实现与运作方式
- C++原版调用
- 用C语言来模拟
- 从最简单的开始: 实现 Base2
- 有了虚函数表的Base1, 但没被覆盖
- 有虚函数覆盖的 Base3
- 定义继承类 CDerive1
- 构造各类的全局虚函数表
- 开始! 从 CDerive1 构造一个完整的 Derive1 类
- 完整代码(加了__stdcall即windows端运行的)
- 虚表示意图
- 完整代码(Linux端运行的)
- 修改好能跑出正确结果的cpp代码(Linux端运行)
- 一些解释说明便于理解
- 为什么 pb1 指向 Derive1 中的 Base1 部分?
- 通过gdb调试更好理解foo的底层运行逻辑
- 运行结果
- ABI是什么、UB是什么
前言
大家都应该知道C++的精髓是虚函数吧? 虚函数带来的好处就是: 可以定义一个基类的指针, 其指向一个继承类, 当通过基类的指针去调用函数时, 可以在运行时决定该调用基类的函数还是继承类的函数. 虚函数是实现多态(动态绑定)/接口函数的基础. 可以说: 没有虚函数, C++将变得一无是处!
既然是C++的精髓, 那么我们有必要了解一下她的实现方式吗? 有必要! 既然C++是从C语言的基础上发展而来的, 那么我们可以尝试用C语言来模拟实现吗? 有可能!
接下来, 就是我一步一步地来解析C++的虚函数的实现方式, 以及用C语言对其进行的模拟.
C++对象的内存布局
类对象的大小就是所有成员变量大小之和(严格说是成员变量内存对齐之后的大小之和).
没有虚函数的对象
拥有仅一个虚函数的类对象
现在, 我们通过VS2013来瞧瞧类Base1的变量b1的内存布局情况:
- 由于我没有写构造函数, 所以变量的数据没有根据, 但虚函数是编译器为我们构造的, 数据正确!
- Debug模式下, 未初始化的变量值为0xCCCCCCCC, 即:-858983460
对象└── __vfptr (void** 类型)↓vtable (里面是函数地址)
对象内的 __vfptr↓vtable数组└── { &Base1::base1_fun1 }
大家有没有留意这个 __vfptr ? 为什么它被定义成一个 指向指针数组的指针, 而不是直接定义成一个 指针数组 呢? 我为什么要提这样一个问题? 因为如果仅是一个指针的情况, 您就无法轻易地修改那个数组里面的内容, 因为她并不属于类对象的一部分. 属于类对象的, 仅是一个指向虚函数表的一个指针__vfptr而已, 下一节我们将继续讨论这个问题.
注意到__vfptr前面的const修饰. 她修饰的是那个虚函数表, 而不是__vfptr。
现在的对象布局如下:
虚函数指针__vfptr位于所有的成员变量之前定义.
注意到: 我并未在此说明__vfptr的具体指向, 只是说明了现在类对象的布局情况.
接下来看一个稍微复杂一点的情况, 我将清楚地描述虚函数表的构成.
拥有多个虚函数的类对象
和前面一个例子差不多, 只是再加了一个虚函数. 定义如下:
class Base1
{
public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {}
};
大小以及偏移信息如下:
有情况!? 多了一个虚函数, 类对象大小却依然是12个字节!
再来看看VS形象的表现:
呀, __vfptr所指向的函数指针数组中出现了第2个元素, 其值为Base1类的第2个虚函数base1_fun2()的函数地址。
现在, 虚函数指针以及虚函数表的伪定义大概如下:
void* __fun[] = { &Base1::base1_fun1, &Base1::base1_fun2 };
const void** __vfptr = __fun[0];
由此我们可以总结出:
同一个类的不同实例共用同一份虚函数表, 她们都通过一个所谓的虚函数表指针__vfptr(定义为void**类型)指向该虚函数表.
是时候该展示一下类对象的内存布局情况了:
单继承且本身不存在虚函数的继承类的内存布局
前面研究了那么多啦, 终于该到研究继承类了! 先研究单继承!
依然, 简单地定义一个继承类, 如下:
class Base1
{
public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {}
};class Derive1 : public Base1
{
public:int derive1_1;int derive1_2;
我们再来看看现在的内存布局(定义为Derive1 d1):
没错! 基类在上边, 继承类的成员在下边依次定义! 展开来看看:
经展开后来看, 前面部分完全就是Base1的东西: 虚函数表指针+成员变量定义. 并且, Base1的虚函数表的[0][1]两项还是其本身就拥有的函数: base1_fun1() 和 base1_fun2()。
现在类的布局情况应该是下面这样:
本身不存在虚函数(不严谨)但存在基类虚函数覆盖的单继承类的内存布局
标题本身不存在虚函数的说法有些不严谨, 我的意思是说: 除经过继承而得来的基类虚函数以外, 自身没有再定义其它的虚函数.
Ok, 既然存在基类虚函数覆盖, 那么来看看接下来的代码会产生何种影响:
class Base1
{
public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {}
};class Derive1 : public Base1
{
public:int derive1_1;int derive1_2;// 覆盖基类函数virtual void base1_fun1() {}
};
可以看到, Derive1类 重写了Base1类的base1_fun1()函数, 也就是常说的虚函数覆盖. 现在是怎样布局的呢?
特别注意我高亮的那一行: 原本是Base1::base1_fun1(), 但由于继承类重写了基类Base1的此方法, 所以现在变成了Derive1::base1_fun1()!
那么, 无论是通过Derive1的指针还是Base1的指针来调用此方法, 调用的都将是被继承类重写后的那个方法(函数), 多态发生了!!!
那么新的布局图:
定义了基类没有的虚函数的单继承的类对象布局
说明一下: 由于前面一种情况只会造成覆盖基类虚函数表的指针, 所以接下来我不再同时讨论虚函数覆盖的情况.
继续贴代码:
class Base1
{
public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {}
};class Derive1 : public Base1
{
public:int derive1_1;int derive1_2;virtual void derive1_fun1() {}
};
和第5类不同的是多了一个自身定义的虚函数. 和第6类不同的是没有基类虚函数的覆盖.
咦, 有没有发现问题? 表面上看来几乎和第5种情况完全一样? 为嘛呢? 现在继承类明明定义了自身的虚函数, 但不见了??
那么, 来看看类对象的大小, 以及成员偏移情况吧:
居然没有变化!!! 前面12个字节是Base1的, 有没有觉得很奇怪?
好吧, 既然表面上没办法了, 我们就只能从汇编入手了, 来看看调用derive1_fun1()时的代码:
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun1();
要注意: 我为什么使用指针的方式调用? 说明一下: 因为如果不使用指针调用, 虚函数调用是不会发生动态绑定的哦! 你若直接 d1.derive1_fun1();, 是不可能会发生动态绑定的, 但如果使用指针: pd1->derive1_fun1(); , 那么 pd1就无从知道她所指向的对象到底是Derive1 还是继承于Derive1的对象, 虽然这里我们并没有对象继承于Derive1, 但是她不得不这样做, 毕竟继承类不管你如何继承, 都不会影响到基类, 对吧?
pd1 → 对象地址└── [0] = __vfptr → 虚函数表地址├── [0] = base1_fun1├── [1] = base1_fun2└── [2] = derive1_fun1 ← ← ← edx + 8
最新的类对象布局表示:
多继承且存在虚函数覆盖同时又存在自身定义的虚函数的类对象布局
真快, 该看看多继承了, 多继承很常见, 特别是接口类中!
依然写点小类玩玩:
class Base1
{
public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {}
};class Base2
{
public:int base2_1;int base2_2;virtual void base2_fun1() {}virtual void base2_fun2() {}
};// 多继承
class Derive1 : public Base1, public Base2
{
public:int derive1_1;int derive1_2;// 基类虚函数覆盖virtual void base1_fun1() {}virtual void base2_fun2() {}// 自身定义的虚函数virtual void derive1_fun1() {}virtual void derive1_fun2() {}
};
代码变得越来越长啦! 为了代码结构清晰, 我尽量简化定义.
初步了解一下对象大小及偏移信息:
貌似, 若有所思? 不管, 来看看VS再想:
好吧, 继承反汇编, 这次的调用代码如下:
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();
反汇编代码如下:
d1 是 Derive1 类型的对象,为什么说它的地址就是 Base1 子对象的起始位置?
为什么是 edx + 0Ch(即 vtable[3])
看看现在的类对象布局图:
如果第1个基类没有虚函数表呢? 进入第9节!
如果第1个直接基类没有虚函数(表)
这次的代码应该比上一个要稍微简单一些, 因为把第1个类的虚函数给去掉了!
class Base1
{
public:int base1_1;int base1_2;
};class Base2
{
public:int base2_1;int base2_2;virtual void base2_fun1() {}virtual void base2_fun2() {}
};// 多继承
class Derive1 : public Base1, public Base2
{
public:int derive1_1;int derive1_2;// 自身定义的虚函数virtual void derive1_fun1() {}virtual void derive1_fun2() {}
};
来看看VS的布局:
这次相对前面一次的图来说还要简单啦! Base1已经没有虚函数表了! (真实情况并非完全这样, 请继续往下看!)
现在的大小及偏移情况: 注意: sizeof(Base1) == 8。
重点是看虚函数的位置, 进入函数调用(和前一次是一样的):
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();
Derive1 对象实际内存布局
下面这个实际布局是错的,但是其他解释是对的
我们可以通过对基类成员变量求偏移来观察:
现在类的布局情况:
What if 两个基类都没有虚函数表
代码如下:
class Base1
{
public:int base1_1;int base1_2;
};class Base2
{
public:int base2_1;int base2_2;
};// 多继承
class Derive1 : public Base1, public Base2
{
public:int derive1_1;int derive1_2;// 自身定义的虚函数virtual void derive1_fun1() {}virtual void derive1_fun2() {}
};
前面吃了个亏, 现在先来看看VS的基本布局:
可以看到, 现在__vfptr已经独立出来了, 不再属于Base1和Base2!
看看求偏移情况:
Ok, 问题解决! 注意高亮的那两行, &d1==&d1.__vfptr, 说明虚函数始终在最前面!
不用再废话, 相信大家对这种情况已经有底了.
对象布局:
如果有三个基类: 虚函数表分别是有, 没有, 有!
这种情况其实已经无需再讨论了, 作为一个完结篇…
上代码:
class Base1
{
public:int base1_1;int base1_2;virtual void base1_fun1() {}virtual void base1_fun2() {}
};class Base2
{
public:int base2_1;int base2_2;
};class Base3
{
public:int base3_1;int base3_2;virtual void base3_fun1() {}virtual void base3_fun2() {}
};// 多继承
class Derive1 : public Base1, public Base2, public Base3
{
public:int derive1_1;int derive1_2;// 自身定义的虚函数virtual void derive1_fun1() {}virtual void derive1_fun2() {}
};
以下是偏移图:
什么Derive1的偏移量是40而不是44?
以下是对象布局图
只需知道: 谁有虚函数表, 谁就往前靠!
C++中父子对象指针间的转换与函数调用
用C语言完全模拟C++虚函数表的实现与运作方式
C++原版调用
以下是对类的改动:
class Base1
{
public:Base1() : base1_1(11) {}int base1_1;virtual void base1_fun1() {std::cout << "Base1::base1_fun1()" << std::endl;}
};class Base2
{
public:Base2() : base2_1(21) {}int base2_1;
};class Base3
{
public:Base3() : base3_1(31) {}int base3_1;virtual void base3_fun1() {std::cout << "Base3::base3_fun1()" << std::endl;}
};class Derive1 : public Base1, public Base2, public Base3
{
public:Derive1() : derive1_1(11) {}int derive1_1;virtual void base3_fun1() {std::cout << "Derive1::base3_fun1()" << std::endl;}virtual void derive1_fun1() {std::cout << "Derive1::derive1_fun1()" << std::endl;}
};
为了看到多态的效果, 我们还需要定义一个函数来看效果:
void foo(Base1* pb1, Base2* pb2, Base3* pb3, Derive1* pd1)
{std::cout << "Base1::\n"<< " pb1->base1_1 = " << pb1->base1_1 << "\n"<< " pb1->base1_fun1(): ";pb1->base1_fun1();std::cout << "Base2::\n"<< " pb2->base2_1 = " << pb2->base2_1<< std::endl;std::cout << "Base3::\n"<< " pb3->base3_1 = " << pb3->base3_1 << "\n"<< " pb3->base3_fun1(): ";pb3->base3_fun1();std::cout << "Derive1::\n"<< " pd1->derive1_1 = " << pd1->derive1_1<< "\n"<< " pd1->derive1_fun1(): ";pd1->derive1_fun1();std::cout<< " pd1->base3_fun1(): ";pd1->base3_fun1();std::cout << std::endl;
}
调用方式如下:
Derive1 d1;
foo(&d1, &d1, &d1, &d1);
输出结果:
可以看到输出结果全部正确(当然了! 😃, 哈哈~
同时注意到 pb3->base3_fun1() 的多态效果哦!
用C语言来模拟
必须要把前面的理解了, 才能看懂下面的代码!
为了有别于已经完成的C++的类, 我们分别在类前面加一个大写的C以示区分(平常大家都是习惯在C++写的类前面加C, 今天恰好反过来, 哈哈).
从最简单的开始: 实现 Base2
由于没有虚函数, 仅有成员变量, 这个当然是最好模拟的咯!
struct CBase2
{int base2_1;
};
有了虚函数表的Base1, 但没被覆盖
下面是Base1的定义, 要复杂一点了, 多一个__vfptr:
struct CBase1
{void** __vfptr;int base1_1;
};
因为有虚函数表, 所以还得单独为虚函数表创建一个结构体的哦! 但是, 为了更能清楚起见, 我并未定义前面所说的指针数组, 而是用一个包含一个或多个函数指针的结构体来表示! 因为数组能保存的是同一类的函数指针, 不太友好! 但他们的效果是完全一样的, 希望读者能够理解明白!
有虚函数覆盖的 Base3
虚函数覆盖在这里并不能体现出来, 要在构造对象初始化的时候才会体现, 所以: base3其实和Base1是一样的.
struct CBase3
{void** __vfptr;int base3_1;
};struct CBase3_VFTable
{void(__stdcall* base3_fun1)(CBase3* that);
};
Base3的成员函数:
void __stdcall base3_fun1(CBase3* that)
{std::cout << "base3_fun1()" << std::endl;
}
定义继承类 CDerive1
相对前面几个类来说, 这个类要显得稍微复杂一些了, 因为包含了前面几个类的内容:
struct CDerive1
{CBase1 base1;CBase3 base3;CBase2 base2;int derive1_1;
};
特别注意: CBase132的顺序不能错!
另外: 由于Derive1本身还有虚函数表, 而且所有项是加到第一个虚函数表(CBase1)的后面的, 所以此时的CBase1::__vfptr不应该单单指向CBase1_VFTable, 而应该指向下面这个包含Derive1类虚函数表的结构体才行:
struct CBase1_CDerive1_VFTable
{void (__stdcall* base1_fun1)(CBase1* that);void(__stdcall* derive1_fun1)(CDerive1* that);
};
因为CDerive1覆盖了CBase3的base3_fun1()函数, 所以不能直接用Base3的那个表:
struct CBase3_CDerive1_VFTable
{void(__stdcall* base3_fun1)(CDerive1* that);
};
Derive1覆盖Base3::base3_fun1()的函数以及自身定义的derive1_fun1()函数:
void __stdcall base3_derive1_fun1(CDerive1* that)
{std::cout << "base3_derive1_fun1()" << std::endl;
}void __stdcall derive1_fun1(CDerive1* that)
{std::cout << "derive1_fun1()" << std::endl;
}
构造各类的全局虚函数表
由于没有了编译器的帮忙, 在定义一个类对象时, 所有的初始化工作都只能由我们自己来完成了!
首先构造全局的, 被同一个类共同使用的虚函数表!
// CBase1 的虚函数表
CBase1_VFTable __vftable_base1;
__vftable_base1.base1_fun1 = base1_fun1;// CBase3 的虚函数表
CBase3_VFTable __vftable_base3;
__vftable_base3.base3_fun1 = base3_fun1;
然后构造CDerive1和CBase1共同使用的虚函数表:
// CDerive1 和 CBase1 共用的虚函数表
CBase1_CDerive1_VFTable __vftable_base1_derive1;
__vftable_base1_derive1.base1_fun1 = base1_fun1;
__vftable_base1_derive1.derive1_fun1 = derive1_fun1;
再构造CDerive1覆盖CBase3后的虚函数表: 注意: 数覆盖会替换原来的函数指针。
CBase3_CDerive1_VFTable __vftable_base3_derive1;
__vftable_base3_derive1.base3_fun1 = base3_derive1_fun1;
开始! 从 CDerive1 构造一个完整的 Derive1 类
先初始化成员变量与__vfptr的指向: 注意不是指错了!
CDerive1 d1;
d1.derive1 = 1;d1.base1.base1_1 = 11;
d1.base1.__vfptr = reinterpret_cast<void**>(&__vftable_base1_derive1);d1.base2.base2_1 = 21;d1.base3.base3_1 = 31;
d1.base3.__vfptr = reinterpret_cast<void**>(&__vftable_base3_derive1);
由于目前的CDerive1是我们手动构造的, 不存在真正语法上的继承关系, 如要得到各基类指针, 我们就不能直接来取, 必须手动根据偏移计算:
char* p = reinterpret_cast<char*>(&d1);
Base1* pb1 = reinterpret_cast<Base1*>(p + 0);
Base2* pb2 = reinterpret_cast<Base2*>(p + sizeof(CBase1) + sizeof(CBase3));
Base3* pb3 = reinterpret_cast<Base3*>(p + sizeof(CBase1));
Derive1* pd1 = reinterpret_cast<Derive1*>(p);
真正调用:
foo(pb1, pb2, pb3, pd1);
调用结果:
完整代码(加了__stdcall即windows端运行的)
#include <iostream>class Base1 {
public:Base1() : base1_1(11) {}int base1_1;// __stdcall: Windows 平台的调用约定:参数右到左压栈,被调用者清理栈virtual void __stdcall base1_fun1() {std::cout << "Base1::base1_fun1()" << std::endl;}
};class Base2 {
public:Base2() : base2_1(21) {}int base2_1;
};class Base3 {
public:Base3() : base3_1(31) {}int base3_1;virtual void __stdcall base3_fun1() {std::cout << "Base3::base3_fun1()" << std::endl;}
};class Derive1 : public Base1, public Base2, public Base3 {
public:Derive1() : derive1_1(11) {}int derive1_1;// 覆盖 base3_fun1()virtual void __stdcall base3_fun1() {std::cout << "Derive1::base3_fun1()" << std::endl;}// 新增 derive1_fun1()virtual void __stdcall derive1_fun1() {std::cout << "Derive1::derive1_fun1()" << std::endl;}
};// 它模拟 C++ 中一个没有虚函数的类 Base2
// 只有一个成员变量 int base2_1
struct CBase2 {int base2_1;
};struct CBase1 {// 模拟 C++ 虚函数表指针void** __vfptr;int base1_1;
};struct CBase1_VFTable {// 这个结构模拟的是 C++ 编译器自动生成的 虚函数表(vtable),本质上是一个函数指针数组/结构。// 这个 base1_fun1 是一个 函数定义,函数名就是 base1_fun1// 这个 base1_fun1 是一个函数指针成员变量,它不是函数,是一个能“存放函数地址”的变量。// 这个结构体中有一个成员,类型是“指向带有一个 CBase1* 参数的 __stdcall 函数”的指针。void (__stdcall* base1_fun1)(CBase1* that);
};// 它本质上是一个普通函数,只不过接受一个 CBase1* 参数来模拟 C++ 的 this 指针。
void __stdcall base1_fun1(CBase1* that) {std::cout << "base1_fun1()" << std::endl;
}struct CBase3 {void** __vfptr;int base3_1;
};struct CBase3_VFTable {void(__stdcall* base3_fun1)(CBase3* that);
};void __stdcall base3_fun1(CBase3* that) {std::cout << "base3_fun1()" << std::endl;
}struct CDerive1 {CBase1 base1;CBase3 base3;CBase2 base2;int derive1_1;
};// 这是模拟 C++ 中 Base1 的虚函数表 在 Derive1 中被覆盖后的版本
struct CBase1_CDerive1_VFTable {void (__stdcall* base1_fun1)(CBase1* that);void (__stdcall* derive1_fun1)(CDerive1* that);
};// 模拟的是 Base3 的虚函数表,其中 base3_fun1 被 Derive1 重写了,所以这张表指向 Derive1 的版本。
struct CBase3_CDerive1_VFTable {void(__stdcall* base3_fun1)(CDerive1* that);
};// 参数是 CDerive1*,意味着这个函数只能通过 Derive1 的对象调用
// 模拟虚函数的“覆盖”
void __stdcall base3_derive1_fun1(CDerive1* that) {std::cout << "base3_derive1_fun1()" << std::endl;
}// 是 Derive1 新增的虚函数 derive1_fun1() 的实现体
void __stdcall derive1_fun1(CDerive1* that) {std::cout << "derive1_fun1()" << std::endl;
}void foo(Base1* pb1, Base2* pb2, Base3* pb3, Derive1* pd1) {std::cout << "Base1::\n"<< " pb1->base1_1 = " << pb1->base1_1 << "\n"<< " pb1->base1_fun1(): ";pb1->base1_fun1();std::cout << "Base2::\n"<< " pb2->base2_1 = " << pb2->base2_1<< std::endl;std::cout << "Base3::\n"<< " pb3->base3_1 = " << pb3->base3_1 << "\n"<< " pb3->base3_fun1(): ";pb3->base3_fun1();std::cout << "Derive1::\n"<< " pd1->derive1_1 = " << pd1->derive1_1 << "\n"<< " pd1->derive1_fun1(): ";pd1->derive1_fun1();std::cout << " pd1->base3_fun1(): ";pd1->base3_fun1();std::cout << std::endl;
}int main() { // CBase1 虚函数表CBase1_VFTable __vftable_base1;__vftable_base1.base1_fun1 = base1_fun1;// CBase3 虚函数表CBase3_VFTable __vftable_base3;__vftable_base3.base3_fun1 = base3_fun1;// CDerive1 对 CBase1 的覆盖虚函数表CBase1_CDerive1_VFTable __vftable_base1_derive1;__vftable_base1_derive1.base1_fun1 = base1_fun1;__vftable_base1_derive1.derive1_fun1 = derive1_fun1;CBase3_CDerive1_VFTable __vftable_base3_derive1;__vftable_base3_derive1.base3_fun1 = base3_derive1_fun1;CDerive1 d1;d1.derive1_1 = 1;d1.base1.base1_1 = 111;// 这样做等效于:C++ 中 Derive1 覆盖了 Base1 的虚函数表// __vfptr 的类型是 void**,而 &__vftable_base1_derive1 的类型是 CBase1_CDerive1_VFTable*。// &__vftable_base1_derive1 是结构体的地址,也就是虚函数表的起始地址// &__vftable_base1_derive1 → 类型是 CBase1_CDerive1_VFTable*// → 底层可以看作 void**/*__vftable_base1_derive1= {(void*)&base1_fun1, // 虚表槽 0(void*)&derive1_fun1 // 虚表槽 1}*/d1.base1.__vfptr = reinterpret_cast<void**>(&__vftable_base1_derive1);d1.base2.base2_1 = 221;d1.base3.base3_1 = 331;d1.base3.__vfptr = reinterpret_cast<void**>(&__vftable_base3_derive1);// 因为你手写的内存布局用的就是这些 C 风格结构(CBase1/CBase3),它们里包含了你手动加的 __vfptr 指针以及可能的对齐/填充。用 sizeof(...) 能自动把对齐算进去,比自己数字节安全。/*&p (=pd1) ─┬─> [CBase1 子对象起始] → pb1├─ + sizeof(CBase1) ───> [CBase3 子对象起始] → pb3├─ + sizeof(CBase1)+sizeof(CBase3) ─> [CBase2 子对象起始] → pb2└─ 整块当 Derive1* 用 → pd1*//*p + 0 // 假定 base1 在最前面p + sizeof(CBase1) // 假定 base3 紧跟其后p + sizeof(CBase1)+sizeof(CBase3)*/char* p = reinterpret_cast<char*>(&d1);Base1* pb1 = reinterpret_cast<Base1*>(p + 0);Base2* pb2 = reinterpret_cast<Base2*>(p + sizeof(CBase1) + sizeof(CBase3));Base3* pb3 = reinterpret_cast<Base3*>(p + sizeof(CBase1));Derive1* pd1 = reinterpret_cast<Derive1*>(p);/*CDerive1:[ CBase1 base1 ] [ CBase3 base3 ] [ CBase2 base2 ] [ int derive1_1 ]^ 偏移0 ^ 偏移offsetof(CDerive1, base3) ...Base1* pb1 = reinterpret_cast<Base1*>(p + offsetof(CDerive1, base1));Base3* pb3 = reinterpret_cast<Base3*>(p + offsetof(CDerive1, base3));Base2* pb2 = reinterpret_cast<Base2*>(p + offsetof(CDerive1, base2));Derive1* pd1 = reinterpret_cast<Derive1*>(p + 0);*/// pb1 指向 Base1 子对象的起始地址(d1.base1)。// pb3 指向 Base3 子对象的起始地址(d1.base3)。// pb2 指向 Base2 子对象的起始地址(d1.base2)。// pd1 指向整块对象的起始地址,你把它当作 Derive1* 用。// d1.base1.__vfptr → __vftable_base1_derive1(槽0= base1_fun1,槽1= derive1_fun1)// d1.base3.__vfptr → __vftable_base3_derive1(槽0= base3_derive1_fun1)// 编译器会把 *(pb3) 当成“Base3 的 vptr”去用——而内存上那几个字节其实就是你放进去的 CBase3::__vfptr。它们是同一片字节,只是你把这片字节当作 Base3 使用而已foo(pb1, pb2, pb3, pd1);return 0;
}
虚表示意图
&d1
├── base1 (带虚表指针)
├── base3 (带虚表指针)
├── base2 (无虚表)
└── derive1_1(自己的成员)
完整代码(Linux端运行的)
#include <iostream>class Base1 {
public:Base1() : base1_1(11) {}int base1_1;virtual void base1_fun1() {std::cout << "Base1::base1_fun1()" << std::endl;}
};class Base2 {
public:Base2() : base2_1(21) {}int base2_1;
};class Base3 {
public:Base3() : base3_1(31) {}int base3_1;virtual void base3_fun1() {std::cout << "Base3::base3_fun1()" << std::endl;}
};class Derive1 : public Base1, public Base2, public Base3 {
public:Derive1() : derive1_1(11) {}int derive1_1;virtual void base3_fun1() {std::cout << "Derive1::base3_fun1()" << std::endl;}virtual void derive1_fun1() {std::cout << "Derive1::derive1_fun1()" << std::endl;}
};// 模拟结构定义(C风格)
struct CBase2 {int base2_1;
};struct CBase1 {void** __vfptr;int base1_1;
};struct CBase1_VFTable {void (*base1_fun1)(CBase1* that);
};void base1_fun1(CBase1* that) {std::cout << "base1_fun1()" << std::endl;
}struct CBase3 {void** __vfptr;int base3_1;
};struct CBase3_VFTable {void (*base3_fun1)(CBase3* that);
};void base3_fun1(CBase3* that) {std::cout << "base3_fun1()" << std::endl;
}struct CDerive1 {CBase1 base1;CBase3 base3;CBase2 base2;int derive1_1;
};struct CBase1_CDerive1_VFTable {void (*base1_fun1)(CBase1* that);void (*derive1_fun1)(CDerive1* that);
};struct CBase3_CDerive1_VFTable {void (*base3_fun1)(CDerive1* that);
};void base3_derive1_fun1(CDerive1* that) {std::cout << "base3_derive1_fun1()" << std::endl;
}void derive1_fun1(CDerive1* that) {std::cout << "derive1_fun1()" << std::endl;
}void foo(Base1* pb1, Base2* pb2, Base3* pb3, Derive1* pd1) {std::cout << "Base1::\n"<< " pb1->base1_1 = " << pb1->base1_1 << "\n"<< " pb1->base1_fun1(): ";pb1->base1_fun1();std::cout << "Base2::\n"<< " pb2->base2_1 = " << pb2->base2_1<< std::endl;std::cout << "Base3::\n"<< " pb3->base3_1 = " << pb3->base3_1 << "\n"<< " pb3->base3_fun1(): ";pb3->base3_fun1();std::cout << "Derive1::\n"<< " pd1->derive1_1 = " << pd1->derive1_1 << "\n"<< " pd1->derive1_fun1(): ";pd1->derive1_fun1();std::cout << " pd1->base3_fun1(): ";pd1->base3_fun1();std::cout << std::endl;
}int main() {// 创建 Base1 的虚表CBase1_VFTable __vftable_base1;__vftable_base1.base1_fun1 = base1_fun1;// 创建 Base3 的虚表CBase3_VFTable __vftable_base3;__vftable_base3.base3_fun1 = base3_fun1;// Derive1 覆盖 Base1 的虚表CBase1_CDerive1_VFTable __vftable_base1_derive1;__vftable_base1_derive1.base1_fun1 = base1_fun1;__vftable_base1_derive1.derive1_fun1 = derive1_fun1;// Derive1 覆盖 Base3 的虚表CBase3_CDerive1_VFTable __vftable_base3_derive1;__vftable_base3_derive1.base3_fun1 = base3_derive1_fun1;// 构造模拟对象CDerive1 d1;d1.derive1_1 = 1;d1.base1.base1_1 = 11;d1.base1.__vfptr = reinterpret_cast<void**>(&__vftable_base1_derive1);d1.base2.base2_1 = 21;d1.base3.base3_1 = 31;d1.base3.__vfptr = reinterpret_cast<void**>(&__vftable_base3_derive1);// 指针偏移访问子对象char* p = reinterpret_cast<char*>(&d1);Base1* pb1 = reinterpret_cast<Base1*>(p + 0);Base2* pb2 = reinterpret_cast<Base2*>(p + sizeof(CBase1) + sizeof(CBase3));Base3* pb3 = reinterpret_cast<Base3*>(p + sizeof(CBase1));Derive1* pd1 = reinterpret_cast<Derive1*>(p);// 编译器会把 *(pb3) 当成“Base3 的 vptr”去用——而内存上那几个字节其实就是你放进去的 CBase3::__vfptr。它们是同一片字节,只是你把这片字节当作 Base3 使用而已foo(pb1, pb2, pb3, pd1);return 0;
}
修改好能跑出正确结果的cpp代码(Linux端运行)
struct CDerive1 {CBase1 base1; // 含 vptr + intCBase2 base2; // 含 intCBase3 base3; // 含 vptr + intint derive1_1;
};
#include <iostream>
#include <cstddef> // offsetofclass Base1 {
public:Base1() : base1_1(11) {}int base1_1;virtual void base1_fun1() {std::cout << "Base1::base1_fun1()" << std::endl;}
};class Base2 {
public:Base2() : base2_1(21) {}int base2_1;
};class Base3 {
public:Base3() : base3_1(31) {}int base3_1;virtual void base3_fun1() {std::cout << "Base3::base3_fun1()" << std::endl;}
};class Derive1 : public Base1, public Base2, public Base3 {
public:Derive1() : derive1_1(123) {}int derive1_1;virtual void base3_fun1() {std::cout << "Derive1::base3_fun1()" << std::endl;}virtual void derive1_fun1() {std::cout << "Derive1::derive1_fun1()" << std::endl;}
};// ---------------- C 风格“模拟对象/虚表” ----------------struct CBase1 {void** __vfptr;int Cbase1_1;
};
struct CBase2 {int Cbase2_1;
};
struct CBase3 {void** __vfptr;int Cbase3_1;
};// 注意顺序:Base1, Base2, Base3, derive1_1 —— 与 Derive1 声明一致
struct CDerive1 {CBase1 base1;CBase2 base2;CBase3 base3;int Cderive1_1;
};// 你自定义的“表项”
struct CBase1_CDerive1_VFTable {void (*base1_fun1)(CBase1* that); // 槽 0void (*derive1_fun1)(CDerive1* that); // 槽 1(派生新增)
};
struct CBase3_CDerive1_VFTable {void (*base3_fun1)(CDerive1* that); // 槽 0(派生覆盖 Base3::base3_fun1)
};// 纯 C 的实现(不使用 this)
void base1_fun1(CBase1*) { std::cout << "base1_fun1()" << std::endl; }
void base3_derive1_fun1(CDerive1*) { std::cout << "base3_derive1_fun1()" << std::endl; }
void derive1_fun1(CDerive1*) { std::cout << "derive1_fun1()" << std::endl; }// ---------------- 演示 ----------------void foo(Base1* pb1, Base2* pb2, Base3* pb3, Derive1* pd1) {// 虚调用是“运行时决定到底调谁”;而你手搓 vtable 的实验能跑,是因为你让编译器从“它以为的 vptr 位置”拿到了“你塞进去的函数地址”。只要哪里和真实 ABI 不一致(尤其是数据成员访问这种静态偏移),就容易炸。// pb1->base1_1 之所以“能用”,是因为它只看 Base1 子对象内部 的固定偏移// 编译器把它展开成:// *(int*)((char*)pb1 + offsetof(Base1, base1_1))// 而你构造的“假对象”里,pb1 指向的是你手搓的 CBase1 子对象起始处,你也确实把 base1_1 放在了 vptr 后面那个位置 → 偏移“刚好对上”,所以表面上是对的。(注意这依然是 UB,只是运气好对齐了。)std::cout << "Base1::\n"<< " pb1->base1_1 = " << pb1->base1_1 << "\n"<< " pb1->base1_fun1(): ";pb1->base1_fun1(); // 走你给 Base1 子对象塞的表(槽 0)std::cout << "Base2::\n"<< " pb2->base2_1 = " << pb2->base2_1 << "\n";std::cout << "Base3::\n"<< " pb3->base3_1 = " << pb3->base3_1 << "\n"<< " pb3->base3_fun1(): ";pb3->base3_fun1(); // 走你给 Base3 子对象塞的表(槽 0)// ↓↓↓ 关键:下面别用编译器的虚调度了,改成“手动查表”// pd1 的静态类型是 Derive1*,如果你用 pd1->derive1_1,编译器会按真·Derive1 的布局去取偏移,跟你手搓的 CDerive1 不同→读错内存// pd1->derive1_1 不行,因为它用的是“真·Derive1 的整体布局偏移”// 编译器把它展开成:// *(int*)((char*)pd1 + offsetof(Derive1, derive1_1))// 但 pd1 并不指向一个真正构造的 Derive1,而是你的 CDerive1 假内存。offsetof(Derive1, derive1_1) 依赖真实 ABI 下 [Base1][Base2][Base3] 的布置、对齐和填充;与你手搓版不保证一致 → 读到的就是垃圾/崩溃。同时也违反了对象生命周期与别名规则(UB)auto cd1 = reinterpret_cast<CDerive1*>(pd1);std::cout << "Derive1::\n"<< " pd1->derive1_1 = " << cd1->Cderive1_1 << "\n";// 从 Base1 子对象的 vptr 里拿槽 1(你手搓的 derive1_fun1)/*你的“假对象”CDerive1 是这样排的:[ CBase1 | CBase2 | CBase3 | derive1_1 ]。CBase1/CBase3 的第一个字段都是 void** __vfptr —— 这就是vptr,指向一个“函数指针数组”(我们手搓的 vtable)。你给 Base1 的那张 vtable 放了两个槽:[0]=base1_fun1, [1]=derive1_fun1。给 Base3 的那张 vtable 放了一个槽:[0]=base3_derive1_fun1*/// void*:虚表里每个槽位是一个 函数地址(可当 void* 理解);// void**:一张虚表是“函数地址数组”的起始指针;这就是 vptr;// void***:对象里那个 __vfptr 字段的地址,是“指向 vptr 的指针”。std::cout << " pd1->derive1_fun1(): ";// pd1 是一个指向 Derive1 类型对象的指针(即 CDerive1)。为了访问 Derive1 的虚表指针,你需要从 pd1 中提取出虚表指针。void** vptr_base1 = *reinterpret_cast<void***>(pd1); // Base1 子对象在首部/*reinterpret_cast<char*>(pd1):把指针变成字节指针,以便做按字节的偏移(标准里只有 char* 能安全做这种原始内存访问)。p + offsetof(CDerive1, base3):跳到 Base3 子对象的起始地址。*reinterpret_cast<void***>(...):同理,取出 Base3 的 vptr(void**)。vptr_base3[0]:取 Base3 那张 vtable 的第 0 槽(你放进去的 base3_derive1_fun1)。强转成 D1Fn,传 cd1 调用。*//*0x1000: base1.__vfptr -> 指向 0x50000x1008: base1.base1_10x1010: base2.base2_10x1018: base3.__vfptr -> 指向 0x60000x1020: base3.base3_10x1028: derive1_1*reinterpret_cast<void***>(0x1000) → 读到 0x5000 = Base1 的 vtable。((void**)0x5000)[1] → 读到 derive1_fun1 的地址,比如 0x7000,然后调用它。p + offsetof(CDerive1, base3) = 0x1000 + 0x18 = 0x1018,*reinterpret_cast<void***>(0x1018) → 读到 0x6000 = Base3 的 vtable。((void**)0x6000)[0] → 读到 base3_derive1_fun1 的地址,比如 0x7100,然后调用它。*/// void** vptr_base1 = *reinterpret_cast<void***>(pd1);// auto cd1 = reinterpret_cast<CDerive1*>(pd1);using D1Fn = void(*)(CDerive1*);reinterpret_cast<D1Fn>(vptr_base1[1])(cd1);// 从 Base3 子对象的 vptr 里拿槽 0(你手搓的 base3_derive1_fun1)std::cout << " pd1->base3_fun1(): ";char* p = reinterpret_cast<char*>(pd1);void** vptr_base3 = *reinterpret_cast<void***>(p + offsetof(CDerive1, base3));reinterpret_cast<D1Fn>(vptr_base3[0])(cd1);std::cout << std::endl;
}int main() {// 准备“派生后的”虚表(注意:这里我们故意不做 ABI 头部,仅演示)CBase1_CDerive1_VFTable __vftable_base1_derive1;__vftable_base1_derive1.base1_fun1 = &base1_fun1;__vftable_base1_derive1.derive1_fun1 = &derive1_fun1;CBase3_CDerive1_VFTable __vftable_base3_derive1;__vftable_base3_derive1.base3_fun1 = &base3_derive1_fun1;// 构造“假对象”CDerive1 d1{};d1.base1.Cbase1_1 = 111;d1.base1.__vfptr = reinterpret_cast<void**>(&__vftable_base1_derive1);d1.base2.Cbase2_1 = 221;d1.base3.Cbase3_1 = 331;d1.base3.__vfptr = reinterpret_cast<void**>(&__vftable_base3_derive1);d1.Cderive1_1 = 1;// 取“子对象指针”// 这种转换常常用于 原始内存操作,比如当你需要以字节为单位(而不是类型的大小)来访问对象的内存时。char* p = reinterpret_cast<char*>(&d1);// offsetof(CDerive1, base1) 计算出 base1 在 CDerive1 对象中的偏移量。由于 base1 是 CDerive1 中的第一个成员(CBase1 base1;),偏移量是 0// offsetof(CDerive1, base1) 计算的是 base1 在 CDerive1 类型对象中的偏移量。// 由于 base1 是 CDerive1 类型中的第一个成员(即 CBase1 base1;),所以 offsetof(CDerive1, base1) 的值应该是 0。这意味着 base1 子对象的内存地址与 CDerive1 对象的起始地址相同。Base1* pb1 = reinterpret_cast<Base1*>(p + offsetof(CDerive1, base1));Base2* pb2 = reinterpret_cast<Base2*>(p + offsetof(CDerive1, base2));Base3* pb3 = reinterpret_cast<Base3*>(p + offsetof(CDerive1, base3));// 如果 pd1 不指向对象起点,这些转换、虚表读取(vptr 在子对象起点)都会错位,整个对象模型就崩了Derive1* pd1 = reinterpret_cast<Derive1*>(p + 0); // 仅作载体传入/*pb1 是 Base1* 类型的指针,指向 CDerive1 类型对象中的 base1 部分。pb2 是 Base2* 类型的指针,指向 CDerive1 类型对象中的 base2 部分。pb3 是 Base3* 类型的指针,指向 CDerive1 类型对象中的 base3 部分。pd1 是 Derive1* 类型的指针,指向整个 CDerive1 对象*/foo(pb1, pb2, pb3, pd1);return 0;
}
在 64 位下,大致内存布局(带常见对齐/填充)示意如下:
CDerive1 d1 (起始地址 = A)偏移(字节) 内容 说明
----------- -------------------------------- -----------------------------
A + 0 base1.__vfptr (8 bytes) Base1 子对象的 vptr
A + 8 base1.base1_1 (4) 你写入 111
A + 12 (填充 4 字节) 让下一个成员按 8 字节对齐A + 16 base2.base2_1 (4) 你写入 221
A + 20 (填充 4 字节)A + 24 base3.__vfptr (8) Base3 子对象的 vptr
A + 32 base3.base3_1 (4) 你写入 331
A + 36 (填充 4 字节)A + 40 derive1_1 (4) 你写入 1
A + 44 (可能有结尾填充)
一些解释说明便于理解
“手搓”了两张虚表(其实就是函数指针数组):
__vftable_base1_derive1:槽 0 -> &base1_fun1 // 打印 "base1_fun1()"槽 1 -> &derive1_fun1 // 打印 "derive1_fun1()"__vftable_base3_derive1:槽 0 -> &base3_derive1_fun1 // 打印 "base3_derive1_fun1()"
并把它们的地址塞进了子对象的 vptr:
d1.base1.__vfptr = &__vftable_base1_derive1;
d1.base3.__vfptr = &__vftable_base3_derive1;
char* p = (char*)&d1; // p == A
Base1* pb1 = (Base1*)(p + offsetof(CDerive1, base1)); // 指向 A+0
Base2* pb2 = (Base2*)(p + offsetof(CDerive1, base2)); // 指向 A+16
Base3* pb3 = (Base3*)(p + offsetof(CDerive1, base3)); // 指向 A+24
Derive1* pd1= (Derive1*)(p + 0); // 直接指到 A
pb1 ──────────► [A + 0 ] base1 子对象起点
pb2 ──────────► [A +16] base2 子对象起点
pb3 ──────────► [A +24] base3 子对象起点
pd1 ──────────► [A + 0 ] 整个“对象”的起点(注意:这不是**真的** Derive1 对象!)
因为 Derive1* 表示“整个对象”的指针,按 C++ 语义它必须指向对象起始地址(也就是这块内存的头 A),而不是某个成员的位置。
A+40 是你对象里成员 derive1_1 的偏移,那是 &pd1->derive1_1 应该指向的地方,不是 pd1 自己
pb1->base1_1
访问数据成员本质是固定偏移读取:
*(int*)((char*)pb1 + offsetof(Base1, base1_1))
为什么 pb1 指向 Derive1 中的 Base1 部分?
通过gdb调试更好理解foo的底层运行逻辑
g++ -g test4.cpp -o test4
pb1=0x7ffffffffdbd0,
pb2=0x7ffffffffdbe0,
pb3=0x7ffffffffdbe8,
pd1=0x7ffffffffdbd0pb1 和 pb2 之间的差距是 16 字节(即 0x10 字节)
0x7ffffffffdbe0 - 0x7ffffffffdbd0 = 0x10pb2 的地址:0x7ffffffffdbe0, pb3 的地址:0x7ffffffffdbe8
这两者之间的差距是 0x8(即 8 个字节)pd1 和 pb3 之间相差 24 字节(即 0x18 字节)
0x7ffffffffdbe8 - 0x7ffffffffdbd0 = 0x18(gdb) print *pb1
$1 = {_vptr.Base1 = 0x7fffffffdbc0, base1_1 = 111}
(gdb) print *pb2
$2 = {base2_1 = 221}
(gdb) print *pb3
$3 = {_vptr.Base3 = 0x7fffffffdb90, base3_1 = 331}
(gdb) print *pd1
$4 = {<Base1> = {_vptr.Base1 = 0x7fffffffdbc0, base1_1 = 111}, <Base2> = {base2_1 = 0}, <Base3> = {_vptr.Base3 = 0xdd, base3_1 = -9328}, derive1_1 = 32767}
pb1 指向 CBase1 类型的子对象,其中包含一个虚表指针 __vfptr,
它指向 __vftable_base1_derive1,即 Base1 的虚表。void** vptr_base1 = pb1->__vfptr;__vfptr 指向 __vftable_base1_derive1,这是一个函数指针数组,
数组的第一个槽存储的是 base1_fun1 函数的地址base1_fun1() 是 Base1 中的虚函数,当你调用 pb1->base1_fun1() 时,
程序需要通过 __vfptr 查找并调用对应的虚函数通过 pb1->__vfptr,程序可以获得虚表的地址,然后从虚表中取出对应的函数地址,最终调用该虚函数。是的,pb1 同时可以是 Base1* 和 CBase1*。这是因为 Base1 和 CBase1 在内存布局上具有很大的相似性,而且你通过 reinterpret_cast 将它们关联起来pd1 是 Derive1* 类型的指针,它指向一个 CDerive1 类型的对象。
CDerive1 模拟了 Derive1 类的内存布局,其中包含了 Base1、Base2 和 Base3 的子对象,以及它们各自的虚表指针。CBase1、CBase2 和 CBase3 是模拟的 Base1、Base2 和 Base3 类,
它们都包含一个虚表指针(__vfptr),指向各自的虚表。
CBase1 的 __vfptr 指向 Base1 的虚表。
CBase3 的 __vfptr 指向 Base3 的虚表pd1 是一个 Derive1* 类型的指针,它指向 CDerive1 对象,实际上就是指向 CBase1 类型的子对象部分。
reinterpret_cast<void***>(pd1) 将 pd1 强制转换为 void*** 类型,这相当于将 pd1 解释为“指向虚表指针的指针”。由于 pd1 是 Derive1*,它指向的对象中第一个成员 CBase1 包含了虚表指针(__vfptr),
所以 reinterpret_cast<void***>(pd1) 实际上让我们访问 CBase1 内的 虚表指针(__vfptr)reinterpret_cast<void***>(pd1) 返回的是 指向 CBase1 中虚表指针(__vfptr)的指针。
通过解引用 *reinterpret_cast<void***>(pd1),我们就得到了 CBase1 的虚表指针 __vfptr,它指向 Base1 的虚表。D1Fn 是一个函数指针类型,指向一个接受 CDerive1* 类型参数并返回 void 的函数。
运行结果
注意:这仍然是“玩 ABI”的把戏,在严格标准意义上依旧是 UB;只是在你当前编译器/平台上可稳定跑通。如果你的函数体开始用 this,多继承分支会涉及 this 调整 thunk,手搓版就得额外处理。想看真·布局与槽位,还是用编译器的布局转储更靠谱。
ABI是什么、UB是什么
之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!