概述
C++中,thunk技术主要用于处理多继承和虚函数调用时复杂的this指针调整问题- 在构造对象的过程中,为虚函数表(
vtable)填充函数指针时,编译器可能会插入thunk代码,以确保后续通过基类指针调用虚函数时,this指针能被正确调整
触发场景
通过非首个基类指针调用派生类重写的虚函数
trunk类型this指针调整thunk
- 核心作用
- 调整
this指针的偏移量,使其指向当前基类子对象在完整派生类对象中的正确位置
- 调整
虚函数返回类型是当前类类型的指针或引用,且涉及多继承
trunk类型- 返回类型调整
thunk
- 返回类型调整
- 核心作用
- 在返回对象前,将
this指针调整回调用方所期望的基类类型的位置
- 在返回对象前,将
trunk工作原理
trunk调用
- 发生多继承时,一个派生类对象在内存中会包含多个基类子对象,如下
Derived类对象内部可能先存储Base1子对象,然后是Base2子对象,最后是Derived自身的成员
|
1 2 3 4 5 6 |
class Base1 { virtual void foo(); }; class Base2 { virtual void foo(); }; class Derived : public Base1, public Base2 { public: virtual void foo() override { /* ... */ } }; |
- 如果你通过一个
Base2*指针来调用foo():- 为了让
Derived::foo()函数能正确访问整个Derived对象的成员(因为foo()是Derived的成员函数) - 在调用它之前,
this指针需要从指向Base2子对象的位置调整到指向完整的Derived对象的起始位置
- 为了让
- 这个调整工作就是由编译器自动生成的
thunk代码完成的- 它是一小段汇编指令,负责进行地址偏移计算,然后跳转到真正的
Derived::foo()函数处执行
- 它是一小段汇编指令,负责进行地址偏移计算,然后跳转到真正的
- 总结而言:
trunk代码的生效,是在运行过程中需要的情况下trunk代码被执行
trunk安装
- 虽然thunk的调用发生在虚函数调用时,但
thunk的“安装”却是在构造函数中完成的- 构造函数的一个重要职责是初始化对象的虚函数表指针(
vptr),使其指向正确的虚函数表(vtable)
- 构造函数的一个重要职责是初始化对象的虚函数表指针(
- 在构造一个派生类对象时,各基类的构造函数会依次执行
- 每个基类的构造函数都会设置
vptr指向其自身的虚函数表 - 当派生类的构造函数执行时,它可能会将
vptr修改为指向派生类的虚函数表
- 每个基类的构造函数都会设置
- 关键在于,在派生类的虚函数表中,对于那些需要
this指针调整的虚函数(例如从第二个及以后的基类继承来的),对应的表项里存放的地址可能并不是虚函数本身的直接地址,而是那个负责调整this指针的thunk的地址- 所以,构造过程中,
thunk的地址被写入虚函数表 - 之后,当通过基类指针调用虚函数时,实际上是通过虚函数表找到了
thunk,由thunk完成指针调整后再跳转到实际的虚函数
- 所以,构造过程中,
其他
单继承关系中,派生类虚函数表对应项存储情况
继承且未重写的基类虚函数- 基类虚函数的直接地址
- 派生类直接使用基类的实现,因此表项指向基类的函数
继承且已重写的基类虚函数- 派生类重写后的虚函数地址
- 实现多态的关键。表项中原先的基类函数地址被派生类的函数地址替换
派生类新增的虚函数- 新增虚函数的直接地址
- 这些新函数的地址会被追加在虚函数表的末尾
单继承关系中,编译器为派生类构建虚函数表的过程
- 复制基类虚表
- 编译器首先会创建一份基类的虚函数表的副本,作为派生类虚函数表的基础
- 替换重写函数
- 接着,如果派生类重写了基类的某个虚函数,那么就在这个副本中,将对应的函数地址
替换为派生类重写后的函数地址 - 这是
C++实现运行时多态(动态绑定)的核心机制
- 接着,如果派生类重写了基类的某个虚函数,那么就在这个副本中,将对应的函数地址
- 追加新增函数
- 最后,如果派生类定义了新的虚函数,这些新函数的地址会被
追加到虚函数表的末尾
- 最后,如果派生类定义了新的虚函数,这些新函数的地址会被
- 示例代码
- 虚函数表索引
0:Derived::func1
重写了基类函数,地址被替换 - 虚函数表索引
1:Base::func2
未重写,保留基类函数地址 - 虚函数表索引
2:Derived::func3
派生类新增的虚函数 - 当你通过一个
Base*指针(它实际指向一个Derived对象)调用func1()时,
程序会通过对象的虚指针(vptr)找到这张虚函数表,并调用索引0处的函数,
也就是Derived::func1(),从而实现了多态
- 虚函数表索引
|
1 2 3 4 5 6 7 8 9 10 11 |
class Base { public: virtual void func1() { /* 基类实现 */ } virtual void func2() { /* 基类实现 */ } }; class Derived : public Base { public: void func1() override { /* 派生类重写实现 */ } // 重写 func1 virtual void func3() { /* 派生类新增虚函数 */ } // 新增虚函数 }; |
单继承关系中,通过基类指针调用派生类虚函数
|
1 |
Base* ptr = new Derived; |
ptr指向Derived对象起始地址- 通过
ptr访问Derived对象前8个字节,获取vptrvptr指向Derived类的vtable
- 在
vtable中按固定索引查找目标函数地址 - 调用该地址指向的
Derived::vfunc
多继承关系中,通过基类指针调用派生类虚函数
- 多继承关系中,通过第一个基类的指针调用被重写的虚函数时
- 具体流程和单继承关系中,通过基类指针调用派生类虚函数的流程相同
|
1 |
Base1* ptr1 = new Derived; |
- 多继承关系中,通过
非第一个基类的指针调用被重写的虚函数时ptr2指向Derived对象中Base2子对象的起始地址- 通过
ptr2访问Base2子对象前8个字节,获取vptr2 vptr2指向Derived类为Base2准备的vtable视图- 在
vtable中按索引查找发现项指向trunk函数 - 调用
trunk函数
trunk执行:计算this指针偏移量
this_derived = this_base2 - offset trunk跳转到实际的Derived::vfunc函数Derived::vfunc执行
使用了正确的this_derived指针
|
1 |
Base2* ptr2 = new Derived; |
多继承关系中,通过非第一个基类的指针调用派生类虚函数时,trunk重新计算this值
- 赋值后
- 指针变量 (
pb2) 的值,指向Derived对象中的Base2子对象
- 指针变量 (
|
1 |
Base2* pb2 = new Derived(); |
- 调用虚函数前
- 指针变量 (
pb2) 的值,保持不变(仍指向Base2子对象)
- 指针变量 (
|
1 |
pb2->virtual_function(); |
Thunk调整- 指针变量 (
pb2) 的值,保持不变(仍指向Base2子对象) - 传入的值,被调整为指向完整
Derived对象的起始地址
- 指针变量 (
|
1 2 3 |
// 在Thunk内部计算 this_derived = this_base2 - offset |
- 函数执行中
- 指针变量 (
pb2) 的值,保持不变(仍指向Base2子对象) - 函数内部使用的是
Thunk传入的调整后的临时值 - 函数内所有成员访问都基于这个正确的
this(临时值)
- 指针变量 (
|
1 |
Derived::virtual_function()内部 |
- 总结
trunk函数内部计算出的正确this值,是一个用于本次函数调用的临时值,它不会更新原始指针变量(如pb2)本身的值
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ C++并发编程 _ 无锁数据结构09/18
- ♥ STL_内存处理工具05/02
- ♥ C++11_第四篇12/08
- ♥ C++标准模板库编程实战_关联容器12/07
- ♥ C++程序高级调试与优化_第一篇07/20
- ♥ C++_解码Toml文件08/14