【C++进阶】一文吃透静态绑定、动态绑定与多态底层机制(含虚函数、vptr、thunk、RTTI)

作者:你的C++教练
日期:2025-08-01


目录

  1. 静态绑定 vs 动态绑定
  2. 非虚函数的三大坑
  3. 多态的四要素
  4. 虚析构函数为什么必须写?
  5. 探秘 vptr/vftablethunk
  6. RTTI 与 dynamic_cast 的底层真相
  7. 虚继承下的虚表偏移
  8. 性能、inline 与构造语义
  9. 实战代码与汇编级分析


1️⃣ 静态绑定 vs 动态绑定

绑定类型 决定时机 典型场景 性能
静态绑定 (Static Binding) 编译期 普通成员函数、缺省实参 直接 call,零额外开销
动态绑定 (Dynamic Binding) 运行期 虚函数通过指针/引用调用 一次 vptr 解引用 + 间接 call

一句话:“指针/引用 + 虚函数”才会触发动态绑定,否则全是静态绑定。


2️⃣ 非虚函数的三大坑

① 普通函数静态绑定
struct B { void foo() { puts("B"); } };
struct D : B { void foo() { puts("D"); } };

B* p = new D;
p->foo();          // 输出 B!(静态绑定)
② 缺省实参静态绑定
struct B {
    virtual void f(int x = 1) { cout << x; }
};
struct D : B {
    void f(int x = 2) override { cout << x; }
};

B* p = new D;
p->f();  // 输出 1!缺省值来自 B 的定义
③ 非虚析构函数 → 内存泄漏
B* p = new D;
delete p;   // 只调 ~B(),~D() 不会被调用

3️⃣ 多态的四要素

条件 说明
继承 存在父子类
虚函数 父类至少一个 virtual
重写 子类覆盖父类虚函数
指针/引用 用父类指针/引用指向子类对象

示例:

class A { public: virtual void vf() { puts("A"); } };
class B : public A { void vf() override { puts("B"); } };

A* p = new B;
p->vf();   // 动态绑定,输出 B

4️⃣ 为什么必须写虚析构函数?

Base* p = new Derived;
delete p;  // 只有 ~Base() 是 virtual,才会:
  1. 先通过 vptr 找到 Derived::~Derived
  2. 执行 ~Derived
  3. 自动插入 ~Base()
  4. 最终 operator delete 释放内存

结论:任何可能被继承的类,析构函数都写成 virtual


5️⃣ 探秘 vptr / vftable / thunk

对象模型(简化)
对象地址
├─ vptr ----┐
├─ 成员变量 │
│           │
v           v
        +--------------+
vftable | &Base::foo   |  <-- 如果未被覆盖
        +--------------+
        | &Derived::bar|
        +--------------+
thunk 是什么?
  • 当用 第二基类指针 指向多重继承的子对象时,需要调整 this 偏移。
  • 编译器生成一段 汇编桩代码(thunk)放在虚表中:
    thunk:
        sub  this, offset   ; 调整 this
        jmp  Derived::foo   ; 真正虚函数
    
  • 虚表项指向的就是 thunk 地址。

6️⃣ RTTI 与 dynamic_cast

if (Derived* d = dynamic_cast<Derived*>(basePtr)) {
    d->onlyInDerived();
}
实现原理
  • 每个有虚函数的类都会在虚表 -1 位置type_info 指针。
  • dynamic_cast 通过 vptr[-1] 比较 RTTI 信息,决定转换是否成功。

7️⃣ 虚继承下的虚表偏移

struct VBase { virtual void vf(); };
struct A : virtual VBase { void vf() override; };
  • 虚基类子对象在内存中可能位于 对象尾部
  • vptr 需要 间接寻址 才能找到虚基类中的虚函数,带来额外一次指针解引用。

8️⃣ 性能 & inline 提醒

因素 开销
虚函数调用 一次额外内存读取
多重继承 可能增加 thunk
虚继承 两次指针解引用
inline 失败 递归、过大、地址取址都会阻止

建议:性能关键路径避免深度虚继承,热点函数尽量 final/inline


9️⃣ 构造语义 & 汇编级分析

伪代码回顾
C::C() {
    B::B();               // 基类构造
        A::A();           // 再基类
            vptr = A::vftable;
        vptr = B::vftable;
    vptr = C::vftable;    // 最终态
}
  • 构造期间 对象类型不断变化,虚表指针逐级覆盖。
  • 析构期间 反向逐级回退,保证 dynamic_cast/typeid 行为正确。

🔚 结论速记

规则 口诀
需要多态 “指针引用 + virtual”
析构函数 “能继承就 virtual”
缺省实参 “静态绑定,别在虚函数里玩默认值”
RTTI “至少一个 virtual 才能 dynamic_cast
性能 “虚函数=一次间接寻址,虚继承=两次”
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐