[量化]《理解 virtual函数:从多态需求到 vtable 内存布局层层解析》

[量化]《理解 virtual函数:从多态需求到 vtable 内存布局层层解析》 目录为什么需要 virtualvirtual 的神秘幕后虚函数表vtable和虚指针vptr调用虚函数时计算机偷偷做了什么一个完整的内存示意图多个虚函数的情况继承链更长的例子纯虚函数和抽象类性能代价总结virtual的使用先看一个没有 virtual 的例子class Animal { public: void speak() { cout 动物叫 endl; } }; class Dog : public Animal { public: void speak() { cout 汪汪 endl; } }; int main() { Animal* p new Dog(); // 基类指针指向派生类对象 p-speak(); // 输出动物叫而不是汪汪 }**结果不符合预期**明明 p 指向的是 Dog 对象却调用了 Animal 的 speak()。这是因为没有 virtual 时编译器根据指针的**静态类型**Animal*决定调用哪个函数在编译期就绑定了早绑定。加上 virtual 就能实现**多态**根据对象的**实际类型**Dog调用正确的函数。class Animal { public: virtual void speak() { cout 动物叫 endl; } }; class Dog : public Animal { public: void speak() override { cout 汪汪 endl; } }; Animal* p new Dog(); p-speak(); // 输出汪汪虚函数表vtable和虚指针vptr编译器为实现 virtual主要做了两件事1. **为每个包含虚函数的类或其派生类生成一个虚函数表vtable**这个表就像一个**函数地址数组**按顺序存放该类所有虚函数的实际地址。2. **在每个对象内部偷偷添加一个虚指针vptr**这个指针指向该类对应的虚函数表。对象的大小会因此增加一个指针的大小64位系统下8字节。### 用图理解文本版类 Animal 的虚函数表vtable for Animal┌─────────────────┐│ Animal::speak() │ ← 存放 speak 函数的地址└─────────────────┘类 Dog 的虚函数表vtable for Dog┌─────────────────┐│ Dog::speak() │ ← 覆盖为 Dog 自己的 speak 地址└─────────────────┘对象的内存布局假设 64 位系统Animal 对象┌──────────────┐│ vptr ────────┼──→ 指向 Animal 的 vtable│ 其他成员... │└──────────────┘Dog 对象┌──────────────┐│ vptr ────────┼──→ 指向 Dog 的 vtable│ 从 Animal 继承的成员... ││ Dog 自己的成员... │└──────────────┘虚函数底层运行原理当你写 p-speak() 时编译器和 CPU 实际执行了以下步骤1. 通过 p 找到所指向对象的**起始地址**。2. 从对象的起始地址读取 **vptr**因为 vptr 通常位于对象最前面。3. 根据 vptr 找到该类的 **vtable**。4. 在 vtable 中speak 函数通常位于第 0 个槽位第一个虚函数。所以从 vtable 地址偏移 0 字节处取出函数地址。5. 调用该地址对应的函数。**等价伪代码**cpp// 编译器将 p-speak() 转换为(*(p-vptr)[0])(p); // 第一个虚函数传入 this 指针整个过程是在**运行时**动态决定的因此叫**动态绑定**。内存示意图假设有类层次- Animal 有一个虚函数 speak()- Dog 继承 Animal覆盖 speak()- Cat 继承 Animal覆盖 speak()内存中的样子Animal 类型的对象 a[a 的 vptr] ────→ [ Animal 的 vtable: Animal::speak ]Dog 类型的对象 d[d 的 vptr] ────→ [ Dog 的 vtable: Dog::speak ]Cat 类型的对象 c[c 的 vptr] ────→ [ Cat 的 vtable: Cat::speak ]用 Animal* p d; p-speak() 流程p 指向 d → 读取 d 的 vptr → 找到 Dog 的 vtable → 取出 Dog::speak 并调用多个虚函数的情况如果类有多个虚函数vtable 里就按声明顺序放多个函数指针。cppclass Animal {virtual void speak();virtual void eat();virtual void sleep();};vtable 像这样Animal 的 vtable┌──────────────┐│ Animal::speak│ (索引 0)├──────────────┤│ Animal::eat │ (索引 1)├──────────────┤│ Animal::sleep│ (索引 2)└──────────────┘调用 p-eat() 时编译器会生成(*(p-vptr)[1])(p)。继承链更长的例子cppclass Animal {virtual void speak();};class Dog : public Animal {void speak() override;};class Husky : public Dog {void speak() override;};- Animal 的 vtable 有 Animal::speak- Dog 的 vtable 覆盖第 0 项为 Dog::speak- Husky 的 vtable 覆盖第 0 项为 Husky::speak对象 Husky 的 vptr 指向 Husky 的 vtable所以调用 speak() 就会执行 Husky::speak。纯虚函数和抽象类virtual void speak() 0; 是纯虚函数。- 编译器会在 vtable 的对应位置放一个**特殊值**通常是 nullptr 或一个错误函数地址。- 拥有纯虚函数的类叫抽象类不能实例化。因为如果创建对象vtable 里那个空位置会导致调用崩溃。性能代价- **内存代价**每个对象多一个指针vptr每个类多一张虚函数表只一份。- **时间代价**调用虚函数比普通函数多两步通过 vptr 取 vtable再通过 vtable 取函数地址间接寻址并且无法内联。但通常代价很小换来强大的多态能力非常值得。总结 **virtual 让编译器为每个类生成一张虚函数表地址数组并为每个对象添加一个隐藏指针指向该表调用虚函数时通过对象的隐藏指针找到表再根据表找到实际要调用的函数从而实现在运行时根据对象真实类型动态决定调用哪个函数。**希望这个解释让你彻底明白了 virtual 的实现原理如果有任何疑问欢迎继续追问。