【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 ” |
性能 | “虛函數=一次間接尋址,虛繼承=兩次” |