【C++进阶】一文吃透静态绑定、动态绑定与多态底层机制(含虚函数、vptr、thunk、RTTI)
【C++进阶】一文吃透静态绑定、动态绑定与多态底层机制(含虚函数、vptr、thunk、RTTI)
作者:你的C++教练
日期:2025-08-01
目录
- 静态绑定 vs 动态绑定
- 非虚函数的三大坑
- 多态的四要素
- 虚析构函数为什么必须写?
- 探秘
vptr
/vftable
与thunk
- RTTI 与
dynamic_cast
的底层真相 - 虚继承下的虚表偏移
- 性能、inline 与构造语义
- 实战代码与汇编级分析
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,才会:
- 先通过
vptr
找到Derived::~Derived
- 执行
~Derived
- 自动插入
~Base()
- 最终
operator delete
释放内存
结论:任何可能被继承的类,析构函数都写成
virtual
。
5️⃣ 探秘 vptr
/ vftable
/ thunk
对象模型(简化)
对象地址
├─ vptr ----┐
├─ 成员变量 │
│ │
v v+--------------+
vftable | &Base::foo | <-- 如果未被覆盖+--------------+| &Derived::bar|+--------------+
thunk
是什么?
- 当用 第二基类指针 指向多重继承的子对象时,需要调整
this
偏移。 - 编译器生成一段 汇编桩代码(thunk)放在虚表中:
thunk:sub this, offset ; 调整 thisjmp 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 ” |
性能 | “虚函数=一次间接寻址,虚继承=两次” |