• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2025-10-24 09:49 Aet 隐藏边栏 |   抢沙发  1 
文章评分 1 次,平均分 5.0

概述

  1. C++中,thunk技术主要用于处理多继承和虚函数调用时复杂的this指针调整问题
  2. 在构造对象的过程中,为虚函数表(vtable)填充函数指针时,编译器可能会插入thunk代码,以确保后续通过基类指针调用虚函数时,this指针能被正确调整

触发场景

通过非首个基类指针调用派生类重写的虚函数

  1. trunk类型
    1. this指针调整 thunk
  2. 核心作用
    1. 调整 this指针的偏移量,使其指向当前基类子对象在完整派生类对象中的正确位置

虚函数返回类型是当前类类型的指针或引用,且涉及多继承

  1. trunk类型
    1. 返回类型调整 thunk
  2. 核心作用
    1. 在返回对象前,将 this指针调整回调用方所期望的基类类型的位置

trunk工作原理

trunk调用

  1. 发生多继承时,一个派生类对象在内存中会包含多个基类子对象,如下
    1. Derived类对象内部可能先存储Base1子对象,然后是Base2子对象,最后是Derived自身的成员

  1. 如果你通过一个Base2*指针来调用foo()
    1. 为了让Derived::foo()函数能正确访问整个Derived对象的成员(因为foo()Derived的成员函数)
    2. 在调用它之前,this指针需要从指向Base2子对象的位置调整到指向完整的Derived对象的起始位置
  2. 这个调整工作就是由编译器自动生成的thunk代码完成的
    1. 它是一小段汇编指令,负责进行地址偏移计算,然后跳转到真正的Derived::foo()函数处执行
  3. 总结而言:
    1. trunk代码的生效,是在运行过程中需要的情况下trunk代码被执行

trunk安装

  1. 虽然thunk的调用发生在虚函数调用时,但thunk的“安装”却是在构造函数中完成的
    1. 构造函数的一个重要职责是初始化对象的虚函数表指针(vptr),使其指向正确的虚函数表(vtable
  2. 在构造一个派生类对象时,各基类的构造函数会依次执行
    1. 每个基类的构造函数都会设置vptr指向其自身的虚函数表
    2. 当派生类的构造函数执行时,它可能会将vptr修改为指向派生类的虚函数表
  3. 关键在于,在派生类的虚函数表中,对于那些需要this指针调整的虚函数(例如从第二个及以后的基类继承来的),对应的表项里存放的地址可能并不是虚函数本身的直接地址,而是那个负责调整this指针的thunk的地址
    1. 所以,构造过程中,thunk的地址被写入虚函数表
    2. 之后,当通过基类指针调用虚函数时,实际上是通过虚函数表找到了thunk,由thunk完成指针调整后再跳转到实际的虚函数

其他

单继承关系中,派生类虚函数表对应项存储情况

  1. 继承且未重写的基类虚函数
    1. 基类虚函数的直接地址
    2. 派生类直接使用基类的实现,因此表项指向基类的函数
  2. 继承且已重写的基类虚函数
    1. 派生类重写后的虚函数地址
    2. 实现多态的关键。表项中原先的基类函数地址被派生类的函数地址替换
  3. 派生类新增的虚函数
    1. 新增虚函数的直接地址
    2. 这些新函数的地址会被追加在虚函数表的末尾

单继承关系中,编译器为派生类构建虚函数表的过程

  1. 复制基类虚表
    1. 编译器首先会创建一份基类的虚函数表的副本,作为派生类虚函数表的基础
  2. 替换重写函数
    1. 接着,如果派生类重写了基类的某个虚函数,那么就在这个副本中,将对应的函数地址替换为派生类重写后的函数地址
    2. 这是C++实现运行时多态(动态绑定) 的核心机制
  3. 追加新增函数
    1. 最后,如果派生类定义了新的虚函数,这些新函数的地址会被追加到虚函数表的末尾
  4. 示例代码
    1. 虚函数表索引0Derived::func1
      重写了基类函数,地址被替换
    2. 虚函数表索引1Base::func2
      未重写,保留基类函数地址
    3. 虚函数表索引2Derived::func3
      派生类新增的虚函数
    4. 当你通过一个 Base*指针(它实际指向一个 Derived对象)调用 func1()时,
      程序会通过对象的虚指针(vptr)找到这张虚函数表,并调用索引0处的函数,
      也就是 Derived::func1(),从而实现了多态

单继承关系中,通过基类指针调用派生类虚函数

  1. ptr指向Derived对象起始地址
  2. 通过ptr访问Derived对象前8个字节,获取vptr
    1. vptr指向Derived类的vtable
  3. vtable中按固定索引查找目标函数地址
  4. 调用该地址指向的Derived::vfunc

多继承关系中,通过基类指针调用派生类虚函数

  1. 多继承关系中,通过第一个基类的指针调用被重写的虚函数时
    1. 具体流程和单继承关系中,通过基类指针调用派生类虚函数的流程相同

  1. 多继承关系中,通过非第一个基类的指针调用被重写的虚函数时
    1. ptr2指向Derived对象中Base2子对象的起始地址
    2. 通过ptr2访问Base2子对象前8个字节,获取vptr2
    3. vptr2指向Derived类为Base2准备的vtable视图
    4. vtable中按索引查找发现项指向trunk函数
    5. 调用trunk函数
      trunk执行:计算this指针偏移量
      this_derived = this_base2 - offset
    6. trunk跳转到实际的Derived::vfunc函数
    7. Derived::vfunc执行
      使用了正确的this_derived指针

多继承关系中,通过非第一个基类的指针调用派生类虚函数时,trunk重新计算this

  1. 赋值后
    1. 指针变量 (pb2) 的值,指向 Derived对象中的 Base2子对象

  1. 调用虚函数前
    1. 指针变量 (pb2) 的值,保持不变(仍指向Base2子对象)

  1. Thunk调整
    1. 指针变量 (pb2) 的值,保持不变(仍指向Base2子对象)
    2. 传入的值,被调整为指向完整Derived对象的起始地址

  1. 函数执行中
    1. 指针变量 (pb2) 的值,保持不变(仍指向Base2子对象)
    2. 函数内部使用的是Thunk传入的调整后的临时值
    3. 函数内所有成员访问都基于这个正确的this(临时值)

  1. 总结
    1. trunk函数内部计算出的正确this值,是一个用于本次函数调用的临时值,它不会更新原始指针变量(如pb2)本身的值

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享