【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 ; 调整 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” |
| 性能 | “虚函数=一次间接寻址,虚继承=两次” |
更多推荐
所有评论(0)