多態意為「多種形態」,核心口號是「一個介面,多種實作」。程式在執行期根據物件實際型別決定呼叫哪個函數版本,這是面向對象程式設計的核心概念。多態與非多態的本質區別在於函數位址綁定時機:
以 virtual 宣告的成員函數為虛擬函數。編譯器為每個含虛擬函數的類別建立虛擬函數表,儲存該類別所有虛擬函數的位址。每個物件內含一隱藏指標(vptr)指向其類別的虛擬函數表,使物件大小增加(32 位元系統 4 bytes)。
衍生類別覆寫虛擬函數時,函數名稱、參數列表、回傳型別必須一致(回傳型別允許共變:基底回傳基底指標,衍生可回傳衍生指標)。覆寫的函數自動成為虛擬函數。
class A {
public:
void foo() { printf("1\n"); }
virtual void fun() { printf("2\n"); }
};
class B : public A {
public:
void foo() { printf("3\n"); }
void fun() { printf("4\n"); } // 覆寫虛擬函數
};
int main() {
A a; B b;
A* p = &a;
p->foo(); // 1 (非虛擬,早期綁定)
p->fun(); // 2 (虛擬,動態綁定到 A::fun)
p = &b;
p->foo(); // 1 (非虛擬,指標型別為 A*,固定呼叫 A::foo)
p->fun(); // 4 (虛擬,動態綁定到 B::fun)
}
關鍵觀察:非虛擬函數 foo() 永遠根據指標靜態型別呼叫;虛擬函數 fun() 根據物件動態型別呼叫。
C++ 隱藏規則導致衍生類別同名函數遮蔽基底同名函數,與是否虛擬無關:
class Base {
virtual void f(float); // 虛擬
void g(float); // 非虛擬
void h(float); // 非虛擬
};
class Derived : public Base {
virtual void f(float); // 覆寫
void g(int); // 隱藏 Base::g(float) — 非多載!
void h(float); // 隱藏 Base::h(float) — 非覆寫!
};
對策:衍生類別需明確 using Base::g; 引入被隱藏名稱,或完整實作所有多載版本。
dynamic_cast<Derived*>(base_ptr):安全向下轉型。成功回傳衍生指標,失敗回傳 nullptr。參考版本失敗拋出 bad_cast。要求類別有虛擬函數。typeid(expr):回傳 const type_info&。type_info::name() 取得型別名稱(編譯器相依)。支援 ==/!= 比較。類別有虛擬函數時利用多態識別動態型別。以 virtual ReturnType func() = 0; 宣告的函數為純虛擬函數(無函數本體)。含純虛擬函數的類別為抽象類別,無法直接實例化物件。抽象類別作為介面規範,強制衍生類別實作純虛擬函數,實現介面契約。
引入原因:
透過基底指標 delete 衍生物件時,若基底解構子非虛擬,僅呼叫基底解構子導致衍生部分資源洩漏。基底類別解構子必須宣告為 virtual。解構子可為純虛擬(但必須提供定義);建構子不可為虛擬(建構時型別已確定,虛擬表未就緒)。
| 分類 | 實現機制 | 綁定時機 | 典型例子 |
|---|---|---|---|
| 編譯期多態 | 函數多載、樣板 | 編譯期 | max(int, int) vs max(double, double) |
| 執行期多態 | 虛擬函數 | 執行期 | 基底指標呼叫衍生覆寫函數 |
virtual。override (C++11) 明確語意並獲編譯器檢查。using Base::func;。