• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2026-03-26 17:02 Aet 隐藏边栏 |   抢沙发  1 
文章评分 0 次,平均分 0.0

静态类型

概述

  1. 静态类型是一个表达式在编译期就确定的类型,它由声明决定,永远不会改变
  2. 示例
    1. 无论运行时 p 实际指向什么,p 的静态类型始终是 Animal*,这是编译器在词法分析阶段就锁死的

什么时候确定

  1. 编译期,词法/语义分析阶段
    1. 编译器看到变量声明时,立刻将类型信息记入符号表(Symbol Table

存在哪里

  1. 静态类型不存在于运行时的任何内存中,它只活在编译阶段
  2. 编译完成后,静态类型信息被用于:
    1. 生成正确的函数调用指令(非虚函数直接绑定地址)
    2. 类型检查(不合法的赋值/转换在此报错)
    3. 决定非虚函数调用走哪个版本

动态类型

概述

  1. 动态类型是指针或引用在运行时实际指向(或绑定)的那个完整对象的类型,只对指针和引用有意义

  1. 对于非指针非引用的普通对象,静态类型 == 动态类型,不存在区别:

什么时候确定

  1. 确定时机:运行时,对象构造时
    1. 动态类型通过 vptr(虚函数表指针) 来携带,在构造函数执行期间被写入对象

存在哪里

  1. 对象内存的头部(vptr
    1. 每个含有虚函数的类的对象,内存布局的最开头(通常)有一个隐藏的指针 vptr,它指向该类的 vtable(虚函数表)

  1. vptr 就是动态类型的"运行时身份证",它指向哪张 vtable,对象的动态类型就是哪个类

vtable 存在哪里

  1. vtable 是一张函数指针数组,由编译器在编译期为每个多态类生成,存放在程序的只读数据段(.rodata)中
  2. 每个类只有一份,所有该类的对象共享

运行时确定动态类型的详细过程

  1. 编译器的准备工作(运行前)
    1. 编译器为每个多态类生成 vtable,并在每个构造函数中插入 vptr 赋值指令
    2. 这一步是自动的,程序员看不到,但可以从汇编中观察到

  1. 对象构造时 vptr 的写入过程
    1. 这就是为什么不应该在构造函数中调用虚函数:
    2. 基类构造阶段,vptr 还指向基类的 vtable,虚函数不会多态分发

  1. 运行时虚函数调用的分发过程

  1. 析构时 vptr 的逆向复原
    1. 析构的顺序和构造相反,vptr 也会被逐步还原:

问题

在构造函数中调用普通虚函数

  1. 调用的虚函数,基类有这个虚函数
    1. 输出 Animal::speak(),不是 Dog::speak()
    2. Animal::Animal() 执行期间,对象的动态类型就是 Animal,因为此时 vptr 指向 Animal::vtable
    3. Dog 的部分尚未构造,编译器通过 vptr 赋值时机保证了这个行为的一致性

  1. 调用的虚函数,基类没有这个虚函数,是派生类的虚函数
    1. 先明确:基类根本调不到派生类的独有虚函数
    2. 基类不知道派生类的存在,所以基类构造函数中无法直接调用派生类独有的虚函数:
    3. 因此,这种情况真正有意义的场景是(见下面的场景:三层继承):
      派生类自己在构造函数中调用自己新增的虚函数,而这个虚函数可能又被更下一级的派生类重写

  1. 场景设定:三层继承
    1. 输出 Dog::fetch(),不是 Labrador::fetch()

  1. 表现
    1. 对于普通虚函数,确实只是多态不生效,退化为调用当前类的版本,不会崩溃
  2. 总结
    1. 在某个类的构造函数执行期间,对象的动态类型就是那个类本身,vptr 指向那个类的 vtable,虚函数分发只到那个类为止,不会穿透到尚未构造的子类

在构造函数中调用纯虚函数

  1. 基类构造函数调用了纯虚函数

  1. 为什么会崩溃:vtable 里放的是什么
    1. 编译器对纯虚函数的处理是:
    2. Animal::vtable 的对应槽位,不放真实函数地址,而是放一个特殊的错误处理函数的地址

  1. __cxa_pure_virtual 是运行时库提供的一个函数,它的实现只做一件事:

  1. 完整崩溃过程

vtable里面存的是函数地址

  1. vtable 里存的就是函数在代码段(.text)的地址
    1. vtable.rodata(只读数据段),它里面的每一项是一个函数指针,指向 .text 里对应函数的机器码起始地址

虚函数在 vtable 中的偏移是怎么确定的

  1. 结论
    1. 偏移量不是运行时计算的,是编译器在编译期静态分配好的,编译器为每个虚函数分配一个固定的槽位索引,调用时直接用这个索引 × 指针大小作为偏移
  2. 编译器如何分配槽位

  1. 多态正确工作的关键:

    1. speak() 无论在哪个类的 vtable 里,都在槽位 0
    2. 所以编译器生成 p->speak() 时,只需写死"取偏移 0 的函数指针",运行时换不同的 vtable 就自动得到不同的实现
  2. 偏移量在哪里体现:看汇编

示例

代码

汇编

  1. x86-64 gcc 15.2环境下编出的汇编
    1. https://godbolt.org

vtable 的真实布局

  1. vtable 开头有两个隐藏字段(各占 8 字节),所以 vptr 不是指向 vtable 的第 0 字节,而是指向 vtable+16,也就是第一个函数指针的位置

构造函数写入 vptr

调用 pt->speak()

解析

  1. 关于vtable
    1. OFFSET 的意思是:把 (vtable起始地址 + 16) 这个地址值本身装入 edx
    2. FLAT 指的是平坦内存模型(Flat Memory Model),x86-64 下所有地址都在同一个线性地址空间里,不需要段寄存器来寻址
    3. 这里只是 GAS 汇编语法的一个修饰符,表示"用平坦地址空间",实际上就是普通的内存地址,可以直接忽略这个词

  1. +16 是什么
    1. 跳过 vtable 的两个隐藏字段,让 vptr 直接指向第一个虚函数槽,这样后续取槽位时偏移计算更干净:
    2. vptr + 0 → speak() 槽位0
    3. vptr + 8 → move() 槽位1
    4. vptr + 16 → eat() 槽位2

  1. OFFSET FLAT:vtable for Animal+16
    1. 是一个编译期地址常量,值是 vtable 起始地址加 16 字节,跳过了 offset-to-toptypeinfo 两个隐藏字段,直接指向 vtable 中第一个虚函数指针的存放位置
    2. vptr 存的就是这个地址
  2. new返回的堆空间的地址是存在rax里面的
    1. 之所以又要从rbx复制到rdi,是因为接下来要调用函数了
    2. rdi到时候可以作为参数去读取

关于嵌套函数调用过程中寄存器的分配问题

  1. callee-saved 寄存器的约定
    1. rbxcallee-saved,意思是:如果 xy 函数内部要用 rbx,必须在用之前先把 rbx 的旧值压栈保存,函数返回前再恢复
    2. tet()rbx 值在 xy() 执行期间被压到栈上"冬眠",xy() 返回后自动恢复,tet() 完全感知不到

  1. x86-64 全部 callee-saved 寄存器

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

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

发表评论

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