静态类型
概述
- 静态类型是一个表达式在编译期就确定的类型,它由声明决定,永远不会改变
- 示例
- 无论运行时
p实际指向什么,p的静态类型始终是Animal*,这是编译器在词法分析阶段就锁死的
- 无论运行时
|
1 2 3 4 5 6 |
class Animal { ... }; class Dog : public Animal { ... }; Animal* p; // p 的静态类型永远是 Animal* Animal& ref = *p; // ref 的静态类型永远是 Animal& Dog d; // d 的静态类型永远是 Dog |
什么时候确定
- 编译期,词法/语义分析阶段
- 编译器看到变量声明时,立刻将类型信息记入符号表(
Symbol Table)
- 编译器看到变量声明时,立刻将类型信息记入符号表(
存在哪里
- 静态类型不存在于运行时的任何内存中,它只活在编译阶段
- 编译完成后,静态类型信息被用于:
- 生成正确的函数调用指令(非虚函数直接绑定地址)
- 类型检查(不合法的赋值/转换在此报错)
- 决定非虚函数调用走哪个版本
动态类型
概述
- 动态类型是指针或引用在运行时实际指向(或绑定)的那个完整对象的类型,只对指针和引用有意义
|
1 2 |
Animal* p = new Dog(); // 静态类型:Animal*,动态类型:Dog p = new Animal(); // 静态类型:Animal*,动态类型:Animal(变了) |
- 对于非指针非引用的普通对象,静态类型 == 动态类型,不存在区别:
|
1 |
Dog d; // 静态类型 Dog == 动态类型 Dog,永远如此 |
什么时候确定
- 确定时机:运行时,对象构造时
- 动态类型通过
vptr(虚函数表指针) 来携带,在构造函数执行期间被写入对象
- 动态类型通过
存在哪里
- 对象内存的头部(
vptr)- 每个含有虚函数的类的对象,内存布局的最开头(通常)有一个隐藏的指针
vptr,它指向该类的vtable(虚函数表)
- 每个含有虚函数的类的对象,内存布局的最开头(通常)有一个隐藏的指针
|
1 2 3 4 5 6 7 8 |
Dog 对象的内存布局: ┌─────────────────────────────┐ │ vptr ──────────────────────┼──► Dog::vtable ├─────────────────────────────┤ │ │ Animal 部分的数据成员 │ ├─ Dog::speak() ├─────────────────────────────┤ ├─ Dog::move() │ Dog 自己的数据成员 │ └─ ... └─────────────────────────────┘ |
vptr就是动态类型的"运行时身份证",它指向哪张vtable,对象的动态类型就是哪个类
vtable 存在哪里
vtable是一张函数指针数组,由编译器在编译期为每个多态类生成,存放在程序的只读数据段(.rodata)中- 每个类只有一份,所有该类的对象共享
|
1 2 3 4 5 6 7 8 9 10 11 |
内存分区示意: ───────────────────────── .text │ 代码 ───────────────────────── .rodata │ Animal::vtable ← 编译期生成,运行时只读 │ Dog::vtable ───────────────────────── .heap │ new Dog() 的对象 ← vptr 在这里,指向上面的 vtable ───────────────────────── .stack │ 局部变量 ───────────────────────── |
运行时确定动态类型的详细过程
- 编译器的准备工作(运行前)
- 编译器为每个多态类生成
vtable,并在每个构造函数中插入vptr赋值指令 - 这一步是自动的,程序员看不到,但可以从汇编中观察到
- 编译器为每个多态类生成
|
1 2 3 4 5 6 7 8 9 |
class Animal { public: virtual void speak() { } }; class Dog : public Animal { public: void speak() override { } }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 编译器悄悄将构造函数改写为(伪代码): // 编译器生成的 Animal 构造函数(伪代码) Animal::Animal() { this->vptr = &Animal::vtable; // ← 编译器插入,你看不到 // ... 你写的构造函数体 } // 编译器生成的 Dog 构造函数(伪代码) Dog::Dog() { Animal::Animal(this); // 先调用基类构造 this->vptr = &Dog::vtable; // ← 再覆盖 vptr!关键! // ... 你写的构造函数体 } |
- 对象构造时
vptr的写入过程- 这就是为什么不应该在构造函数中调用虚函数:
- 基类构造阶段,
vptr还指向基类的vtable,虚函数不会多态分发
|
1 |
Animal* p = new Dog(); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 这一行触发的完整序列: 1. operator new 在堆上分配 sizeof(Dog) 字节的内存 ┌─────────────────────┐ │ ???? │ ???? │ │ ← 全是未初始化数据 └─────────────────────┘ 2. 调用 Animal::Animal() → 编译器插入:this->vptr = &Animal::vtable ┌─────────────────────┐ │ Animal │ Animal │ ← vptr 指向 Animal::vtable │ vptr │ data │ └─────────────────────┘ 此刻如果在基类构造函数中调用虚函数,动态类型是 Animal! 3. 调用 Dog::Dog() → 编译器插入:this->vptr = &Dog::vtable ← 覆盖! ┌─────────────────────┐ │ Dog │ Animal │ Dog │ ← vptr 现在指向 Dog::vtable │ vptr │ data │ data │ └─────────────────────┘ 构造完成,动态类型确立为 Dog 4. 返回指针,p(静态类型 Animal*)指向这个对象 |
- 运行时虚函数调用的分发过程
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
p->speak(); // p 的静态类型是 Animal* // 编译器看到这是虚函数调用,生成如下逻辑(对应的汇编伪代码): 1. 读取对象头部的 vptr mov rax, [p] ; rax = p 指向的对象的第一个字段,即 vptr 2. 根据 speak() 在 vtable 中的偏移量,取出函数地址 mov rax, [rax + 0] ; 偏移量由编译器在编译期确定,speak() 是第0项 ; rax 现在是 Dog::speak 的地址 3. 调用 call rax ; 动态分发,实际执行 Dog::speak() |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 整个过程用一张图总结: p(Animal*,静态类型) │ │ 运行时解引用 ▼ ┌──────────────────────────┐ │ vptr ────────────────────┼──► Dog::vtable(.rodata) │ Animal::data │ ├─[0] Dog::speak ◄── 取这里 │ Dog::data │ ├─[1] Dog::move └──────────────────────────┘ └─ ... ↑ 动态类型就是 "vptr 指向哪张 vtable" 这张表属于 Dog, 所以动态类型是 Dog |
- 析构时
vptr的逆向复原- 析构的顺序和构造相反,
vptr也会被逐步还原:
- 析构的顺序和构造相反,
|
1 2 3 4 5 6 7 8 9 10 11 12 |
delete p; 1. 调用 Dog::~Dog() → 编译器插入:this->vptr = &Dog::vtable (析构体执行期间) → 执行 Dog 的析构体 → 编译器插入:this->vptr = &Animal::vtable (即将调用基类析构前) 2. 调用 Animal::~Animal() → vptr 已经是 Animal::vtable → 执行 Animal 的析构体 3. operator delete 释放内存 |
问题
在构造函数中调用普通虚函数
- 调用的虚函数,基类有这个虚函数
- 输出
Animal::speak(),不是Dog::speak() - 在
Animal::Animal()执行期间,对象的动态类型就是Animal,因为此时vptr指向Animal::vtable Dog的部分尚未构造,编译器通过vptr赋值时机保证了这个行为的一致性
- 输出
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Animal { public: Animal() { speak(); // 在基类构造函数中调用虚函数 } virtual void speak() { cout << "Animal::speak()" << endl; } }; class Dog : public Animal { public: Dog() { } void speak() override { cout << "Dog::speak()" << endl; } }; // 执行: Animal* p = new Dog(); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// 执行过程详解 new Dog() 触发的构造序列: ┌─────────────────────────────────────────────────┐ │ Step 1:分配内存 │ │ 堆上分配 sizeof(Dog) 字节 │ │ ┌──────────────────────┐ │ │ │ ???? │ ???? │ │ ← 未初始化 │ │ └──────────────────────┘ │ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ Step 2:调用 Animal::Animal() │ │ 编译器插入:this->vptr = &Animal::vtable │ │ ┌──────────────────────┐ │ │ │ Animal::vptr │ data │ │ │ └──────┬───────────────┘ │ │ └──► Animal::vtable │ │ [0] Animal::speak ← 此刻表里 │ │ 只有这个 │ │ │ │ ⚡ 执行 speak(): │ │ 读 vptr → Animal::vtable │ │ 取 [0] → Animal::speak 地址 │ │ 调用 → Animal::speak() ✓ 调用的是基类版本│ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ Step 3:调用 Dog::Dog() │ │ 编译器插入:this->vptr = &Dog::vtable ← 覆盖! │ │ ┌──────────────────────┐ │ │ │ Dog::vptr │ data │ │ │ └──────┬──────────────-┘ │ │ └──► Dog::vtable │ │ [0] Dog::speak ← 现在变了 │ └─────────────────────────────────────────────────┘ |
- 调用的虚函数,基类没有这个虚函数,是派生类的虚函数
- 先明确:基类根本调不到派生类的独有虚函数
- 基类不知道派生类的存在,所以基类构造函数中无法直接调用派生类独有的虚函数:
- 因此,这种情况真正有意义的场景是(见下面的场景:三层继承):
派生类自己在构造函数中调用自己新增的虚函数,而这个虚函数可能又被更下一级的派生类重写
|
1 2 3 4 5 6 7 8 9 10 11 |
class Animal { public: Animal() { fetch(); // ❌ 编译错误:Animal 根本没有 fetch() } }; class Dog : public Animal { public: virtual void fetch() { } // Dog 独有,Animal 不知道 }; |
- 场景设定:三层继承
- 输出
Dog::fetch(),不是Labrador::fetch()
- 输出
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Animal { public: Animal() { } virtual void speak() { } // Animal 有 }; class Dog : public Animal { public: Dog() { fetch(); // Dog 在自己的构造函数中调用自己新增的虚函数 } virtual void fetch() { // Dog 新增,Animal 没有 cout << "Dog::fetch()" << endl; } }; class Labrador : public Dog { public: Labrador() { } void fetch() override { // Labrador 重写了 fetch cout << "Labrador::fetch()" << endl; } }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// 执行: Dog* p = new Labrador(); new Labrador() 触发的完整构造序列: ┌──────────────────────────────────────────────────────────┐ │ Step 1:Animal::Animal() │ │ this->vptr = &Animal::vtable │ │ ┌────────────────────────────────────────────┐ │ │ │ vptr │ Animal data │ │ │ └────┬───────────────────────────────────────┘ │ │ └──► Animal::vtable │ │ [0] Animal::speak │ │ (fetch 根本不在这张表里) │ └──────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────┐ │ Step 2:Dog::Dog() │ │ this->vptr = &Dog::vtable ← 覆盖为 Dog 的表 │ │ ┌────────────────────────────────────────────┐ │ │ │ vptr │ Animal data │ Dog data │ │ │ └────┬───────────────────────────────────────┘ │ │ └──► Dog::vtable │ │ [0] Animal::speak (继承来的) │ │ [1] Dog::fetch ← fetch 在这里 │ │ │ │ ⚡ 执行 fetch(): │ │ 读 vptr → Dog::vtable │ │ 取 [1] → Dog::fetch 地址 │ │ 调用 → Dog::fetch() ← 调用的是 Dog 版本! │ │ 不是 Labrador::fetch() │ └──────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────┐ │ Step 3:Labrador::Labrador() │ │ this->vptr = &Labrador::vtable ← 再次覆盖 │ │ ┌────────────────────────────────────────────┐ │ │ │ vptr │ Animal data │ Dog data │ Lab data │ │ │ └────┬───────────────────────────────────────┘ │ │ └──► Labrador::vtable │ │ [0] Animal::speak │ │ [1] Labrador::fetch ← 现在才变成这个 │ └──────────────────────────────────────────────────────────┘ |
- 表现
- 对于普通虚函数,确实只是多态不生效,退化为调用当前类的版本,不会崩溃
- 总结
- 在某个类的构造函数执行期间,对象的动态类型就是那个类本身,
vptr指向那个类的vtable,虚函数分发只到那个类为止,不会穿透到尚未构造的子类
- 在某个类的构造函数执行期间,对象的动态类型就是那个类本身,
在构造函数中调用纯虚函数
- 基类构造函数调用了纯虚函数
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Animal { public: Animal() { speak(); // ← 调用了纯虚函数! } virtual void speak() = 0; // 纯虚函数,Animal 自己没有实现 }; class Dog : public Animal { public: void speak() override { cout << "Dog::speak()" << endl; } }; Animal* p = new Dog(); // 💥 崩溃 |
- 为什么会崩溃:
vtable里放的是什么- 编译器对纯虚函数的处理是:
- 在
Animal::vtable的对应槽位,不放真实函数地址,而是放一个特殊的错误处理函数的地址
|
1 2 3 4 5 6 7 8 9 |
Animal::vtable(运行时实际内容): ┌────────────────────────────────────────────────────┐ │ [0] __cxa_pure_virtual ← 不是真实函数,是"陷阱" │ └────────────────────────────────────────────────────┘ Dog::vtable(构造完成后): ┌────────────────────────────────────────────────────┐ │ [0] Dog::speak ← 正常的函数地址 │ └────────────────────────────────────────────────────┘ |
__cxa_pure_virtual是运行时库提供的一个函数,它的实现只做一件事:
|
1 2 3 4 |
// libstdc++ 的实现本质上就是这样 extern "C" void __cxa_pure_virtual() { abort(); // 直接终止程序 } |
- 完整崩溃过程
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// new Dog() 的执行序列: Step 1:Animal::Animal() this->vptr = &Animal::vtable ┌──────────────────────────────────────────┐ │ vptr ──► Animal::vtable │ │ [0] __cxa_pure_virtual │ └──────────────────────────────────────────┘ ⚡ 执行 speak(): 读 vptr → Animal::vtable 取 [0] → __cxa_pure_virtual 的地址 call → __cxa_pure_virtual() → abort() 💥 程序终止,到不了 Step 2 Step 2:Dog::Dog() ← 永远执行不到 this->vptr = &Dog::vtable ... |
vtable里面存的是函数地址
vtable里存的就是函数在代码段(.text)的地址vtable在.rodata(只读数据段),它里面的每一项是一个函数指针,指向.text里对应函数的机器码起始地址
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
内存分区: ───────────────────────────────────────────── .text │ Animal::speak 的机器码 ← 地址:0x401020 │ Dog::speak 的机器码 ← 地址:0x401080 │ Dog::fetch 的机器码 ← 地址:0x4010F0 ───────────────────────────────────────────── .rodata │ Animal::vtable │ [0] 0x401020 ← 指向 Animal::speak 的代码 │ │ Dog::vtable │ [0] 0x401080 ← 指向 Dog::speak 的代码 │ [1] 0x4010F0 ← 指向 Dog::fetch 的代码 ───────────────────────────────────────────── .heap │ Dog 对象 │ vptr → Dog::vtable ───────────────────────────────────────────── |
虚函数在 vtable 中的偏移是怎么确定的
- 结论
- 偏移量不是运行时计算的,是编译器在编译期静态分配好的,编译器为每个虚函数分配一个固定的槽位索引,调用时直接用这个索引 × 指针大小作为偏移
- 编译器如何分配槽位
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 规则一:基类虚函数,按声明顺序分配槽位 class Animal { virtual void speak() { } // 槽位 0 virtual void move() { } // 槽位 1 virtual void eat() { } // 槽位 2 }; Animal::vtable: ┌─────────────────────────────┐ │ [0] &Animal::speak │ 偏移 0 × 8 = 0 字节 │ [1] &Animal::move │ 偏移 1 × 8 = 8 字节 │ [2] &Animal::eat │ 偏移 2 × 8 = 16 字节 └─────────────────────────────┘ |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 规则二:派生类重写的虚函数,继承基类的槽位,索引不变 class Dog : public Animal { void speak() override { } // 继承槽位 0,覆盖地址 void move() override { } // 继承槽位 1,覆盖地址 // eat() 没重写,槽位 2 继续用 Animal::eat 的地址 }; Animal::vtable: Dog::vtable: ┌──────────────────┐ ┌──────────────────┐ │ [0] Animal::speak│ │ [0] Dog::speak │ ← 地址换了,索引没变 │ [1] Animal::move │ │ [1] Dog::move │ ← 地址换了,索引没变 │ [2] Animal::eat │ │ [2] Animal::eat │ ← 没重写,地址不变 └──────────────────┘ └──────────────────┘ |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 规则三:派生类新增的虚函数,追加到末尾 class Dog : public Animal { void speak() override { } // 槽位 0(继承) void move() override { } // 槽位 1(继承) // eat() 未重写 // 槽位 2(继承) virtual void fetch() { } // 槽位 3(新增,追加到末尾) }; Dog::vtable: ┌──────────────────┐ │ [0] Dog::speak │ 偏移 0 × 8 = 0 字节 │ [1] Dog::move │ 偏移 1 × 8 = 8 字节 │ [2] Animal::eat │ 偏移 2 × 8 = 16 字节 │ [3] Dog::fetch │ 偏移 3 × 8 = 24 字节 ← 新增槽位 └──────────────────┘ |
-
多态正确工作的关键:
speak()无论在哪个类的vtable里,都在槽位0- 所以编译器生成
p->speak()时,只需写死"取偏移0的函数指针",运行时换不同的vtable就自动得到不同的实现
-
偏移量在哪里体现:看汇编
|
1 2 3 |
Animal* p = new Dog(); p->speak(); // 槽位 0 p->move(); // 槽位 1 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
; p->speak():槽位 0,偏移 = 0 * 8 = 0 mov rax, [p] ; rax = vptr(对象头部) mov rax, [rax + 0] ; 取 vtable[0] 的函数地址 call rax ; 调用 ; p->move():槽位 1,偏移 = 1 * 8 = 8 mov rax, [p] ; rax = vptr mov rax, [rax + 8] ; 取 vtable[1] 的函数地址 call rax ; 调用 // [rax + 0]、[rax + 8] 这两个偏移量是编译器在编译期写死在指令里的 // 运行时不需要任何计算 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// 完整的调用全景图 源码:p->speak() │ │ 编译期: │ 1. p 的静态类型是 Animal* │ 2. speak() 是虚函数,查 Animal 的虚函数表定义 │ 3. 确定 speak() 的槽位是 0,偏移 = 0 * 8 = 0 │ 4. 生成指令:取 vptr,再取 vptr[+0],call │ ▼ 运行期: p(Animal*) │ │ 1. 读对象头部 ▼ ┌────────────────────────────┐ │ vptr = 0x602040 │ → Dog::vtable 的地址 │ ... │ └────────────────────────────┘ │ │ 2. 去 vtable,偏移 +0(编译期写死) ▼ Dog::vtable(0x602040) ┌────────────────────────────┐ │ [+0] 0x401080 ← 取这里 │ → Dog::speak 在 .text 的地址 │ [+8] 0x4010A0 │ │ [+16] 0x401020 │ └────────────────────────────┘ │ │ 3. 拿到 0x401080,call ▼ .text(0x401080) ┌────────────────────────────┐ │ Dog::speak() 的机器码 │ ← 实际执行这里 └────────────────────────────┘ |
示例
代码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Animal { public: virtual void speak() { } // 槽位 0 virtual void move() { } // 槽位 1 virtual void eat() { } // 槽位 2 }; class Dog : public Animal { public: void speak() override { } // 槽位 0(继承) void move() override { } // 槽位 1(继承) // eat() 未重写 // 槽位 2(继承) virtual void fetch() { } // 槽位 3(新增,追加到末尾) }; int tet() { Animal* pt = new Dog(); pt->speak(); return 0; } |
汇编
x86-64 gcc 15.2环境下编出的汇编https://godbolt.org
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
Animal::speak(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret Animal::move(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret Animal::eat(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret Dog::speak(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret Dog::move(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret Dog::fetch(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret Animal::Animal() [base object constructor]: push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov edx, OFFSET FLAT:vtable for Animal+16 mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], rdx nop pop rbp ret .set Animal::Animal() [complete object constructor],Animal::Animal() [base object constructor] Dog::Dog() [base object constructor]: push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-8], rdi mov rax, QWORD PTR [rbp-8] mov rdi, rax call Animal::Animal() [base object constructor] mov edx, OFFSET FLAT:vtable for Dog+16 mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], rdx nop leave ret .set Dog::Dog() [complete object constructor],Dog::Dog() [base object constructor] tet(): push rbp mov rbp, rsp push rbx sub rsp, 24 mov edi, 8 call operator new(unsigned long) mov rbx, rax mov QWORD PTR [rbx], 0 mov rdi, rbx call Dog::Dog() [complete object constructor] mov eax, 0 mov QWORD PTR [rbp-24], rbx test al, al je .L10 mov esi, 8 mov rdi, rbx call operator delete(void*, unsigned long) .L10: mov rax, QWORD PTR [rbp-24] mov rax, QWORD PTR [rax] mov rdx, QWORD PTR [rax] mov rax, QWORD PTR [rbp-24] mov rdi, rax call rdx mov eax, 0 mov rbx, QWORD PTR [rbp-8] leave ret vtable for Dog: .quad 0 .quad typeinfo for Dog .quad Dog::speak() .quad Dog::move() .quad Animal::eat() .quad Dog::fetch() vtable for Animal: .quad 0 .quad typeinfo for Animal .quad Animal::speak() .quad Animal::move() .quad Animal::eat() typeinfo for Dog: .quad vtable for __cxxabiv1::__si_class_type_info+16 .quad typeinfo name for Dog .quad typeinfo for Animal typeinfo name for Dog: .string "3Dog" typeinfo for Animal: .quad vtable for __cxxabiv1::__class_type_info+16 .quad typeinfo name for Animal typeinfo name for Animal: .string "6Animal" |
vtable 的真实布局
vtable开头有两个隐藏字段(各占8字节),所以vptr不是指向vtable的第0字节,而是指向vtable+16,也就是第一个函数指针的位置
|
1 2 3 4 5 6 7 |
vtable for Dog: .quad 0 ; [vtable+0] offset-to-top(多继承用) .quad typeinfo for Dog ; [vtable+8] RTTI 类型信息指针 .quad Dog::speak() ; [vtable+16] ← vptr 实际指向这里!槽位0 .quad Dog::move() ; [vtable+24] 槽位1 .quad Animal::eat() ; [vtable+32] 槽位2(未重写,用基类的) .quad Dog::fetch() ; [vtable+40] 槽位3(新增) |
构造函数写入 vptr
|
1 2 3 4 5 6 7 8 9 10 |
Animal::Animal() [base object constructor]: mov edx, OFFSET FLAT:vtable for Animal+16 ; edx = Animal::vtable 第一个函数槽的地址 mov rax, QWORD PTR [rbp-8] ; rax = this mov QWORD PTR [rax], rdx ; this->vptr = &Animal::vtable[0] Dog::Dog() [base object constructor]: call Animal::Animal() [base object constructor] ; 先调基类构造,vptr=Animal::vtable+16 mov edx, OFFSET FLAT:vtable for Dog+16 ; edx = Dog::vtable 第一个函数槽的地址 mov rax, QWORD PTR [rbp-8] ; rax = this mov QWORD PTR [rax], rdx ; this->vptr = &Dog::vtable[0] ← 覆盖! |
调用 pt->speak()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
tet(): ; ── new Dog() 部分 ── mov edi, 8 call operator new(unsigned long) ; 堆上分配 8 字节(只有 vptr,无其他成员) mov rbx, rax ; rbx = 对象地址 mov QWORD PTR [rbx], 0 ; 对象内存清零 mov rdi, rbx call Dog::Dog() [complete object constructor] ; 调构造函数,写入 vptr mov QWORD PTR [rbp-24], rbx ; pt = rbx(存到栈上的 pt 变量) ; ── pt->speak() 部分 ── mov rax, QWORD PTR [rbp-24] ; ① rax = pt(从栈取出 Animal* 指针) mov rax, QWORD PTR [rax] ; ② rax = *pt = vptr = Dog::vtable+16 mov rdx, QWORD PTR [rax] ; ③ rdx = *(vptr+0) = Dog::speak 的地址 mov rax, QWORD PTR [rbp-24] ; ④ rax = pt(重新取,用作 this 指针) mov rdi, rax ; ⑤ rdi = this(x86-64 第一个参数用 rdi) call rdx ; ⑥ call Dog::speak() |
解析
- 关于
vtableOFFSET的意思是:把 (vtable起始地址 + 16) 这个地址值本身装入edxFLAT指的是平坦内存模型(Flat Memory Model),x86-64下所有地址都在同一个线性地址空间里,不需要段寄存器来寻址- 这里只是
GAS汇编语法的一个修饰符,表示"用平坦地址空间",实际上就是普通的内存地址,可以直接忽略这个词
|
1 |
mov edx, OFFSET FLAT:vtable for Animal+16 |
+16是什么- 跳过
vtable的两个隐藏字段,让vptr直接指向第一个虚函数槽,这样后续取槽位时偏移计算更干净: vptr + 0 → speak()槽位0vptr + 8 → move()槽位1vptr + 16 → eat()槽位2
- 跳过
|
1 2 3 4 5 6 7 8 9 10 |
vtable for Animal 在 .rodata 的布局: 地址 内容 ───────────────────────────────────────────────────── vtable + 0 │ 0 (offset-to-top,8字节) vtable + 8 │ &typeinfo for Animal (RTTI指针,8字节) vtable + 16 │ &Animal::speak() ← vptr 指向这里 vtable + 24 │ &Animal::move() vtable + 32 │ &Animal::eat() ───────────────────────────────────────────────────── |
OFFSET FLAT:vtable for Animal+16- 是一个编译期地址常量,值是
vtable起始地址加16字节,跳过了offset-to-top和typeinfo两个隐藏字段,直接指向vtable中第一个虚函数指针的存放位置 vptr存的就是这个地址
- 是一个编译期地址常量,值是
new返回的堆空间的地址是存在rax里面的- 之所以又要从
rbx复制到rdi,是因为接下来要调用函数了 rdi到时候可以作为参数去读取
- 之所以又要从
|
1 2 3 4 5 |
call operator new(unsigned long) mov rbx, rax mov QWORD PTR [rbx], 0 ;清0 mov rdi, rbx call Dog::Dog() [complete object constructor] |
关于嵌套函数调用过程中寄存器的分配问题
callee-saved寄存器的约定rbx是callee-saved,意思是:如果xy函数内部要用rbx,必须在用之前先把rbx的旧值压栈保存,函数返回前再恢复tet()的rbx值在xy()执行期间被压到栈上"冬眠",xy()返回后自动恢复,tet()完全感知不到
|
1 2 3 4 5 6 7 8 9 10 11 |
xy(): push rbp push rbx ← 我要用 rbx 了,先把调用者的 rbx 保存到栈上 mov rbx, ... ← 现在可以放心用 rbx 了 call 其他函数() ... pop rbx ← 返回前恢复 rbx,tet() 的 rbx 不受影响 pop rbp ret |
x86-64全部callee-saved寄存器
|
1 |
rbx, rbp, r12, r13, r14, r15 |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 包管理器:设计与实现09/18
- ♥ C++_智能指针08/31
- ♥ C++并发编程 _ 同步并发(Future)05/22
- ♥ C++_函数模板、类模板、特化、模板元编程、SFINAE、概念06/22
- ♥ STL_stack05/19
- ♥ gflags记述:记录210/09