首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

C++ 進階:避免重新定義繼承而來的非虛擬函式

     編輯:關於C++

現象:預設參數值的靜態繫結陷阱


class Base {
public:
    virtual void function(int arg = 1) { cout << arg << endl; }
};

class Derive : public Base {
public:
    virtual void function() { cout << "Derive" << endl; }
    virtual void function(int arg) { cout << "Derive " << arg << endl; }
};

int main() {
    Base* obj = new Derive();
    obj->function(); // 輸出 "Derive 1"
}

雖然呼叫的是衍生類別的 function(),但預設參數值 **1 來自基底類別**,因為預設參數是**靜態繫結** (編譯期決定),而虛擬函式是**動態繫結** (執行期決定)。

核心概念:靜態類型 vs 動態類型

  • 靜態類型:指標/參考宣告時的類型 (Base*)
  • 動態類型:實際指向的物件類型 (Derive*)

Base* pgo = new Derive; // 靜態: Base*, 動態: Derive*
Asterioid* pa = new Asterioid; // 靜態/動態皆為 Asterioid*
pgo = pa; // 靜態仍為 Base*, 動態變為 Asterioid*
Base& rgo = *pa; // 靜態 Base&, 動態 Asterioid

為什麼 C++ 採用靜態繫結預設參數?

若預設參數採動態繫結,編譯器需在執行期為虛擬函式決定適當預設值,會增加複雜度與效能開銷。C++ 選擇**效率與簡潔**,犧牲此情境的直覺性。

Effective C++ 條款 36:避免重新定義繼承而來的非虛擬函式

非虛擬函式 = 靜態繫結


class B { public: void mf(); };
class D : public B { public: void mf(); }; // 重新定義 (隱藏)

B* pB = new D;
D* pD = new D;
pB->mf(); // 呼叫 B::mf (靜態類型 B*)
pD->mf(); // 呼叫 D::mf (靜態類型 D*)

同一物件、同一函式名稱,卻因指標靜態類型不同而有不同行為 —— **「雙面行為」**。

虛擬函式 = 動態繫結

若 mf 為虛擬函式,pB->mf() 與 pD->mf() 皆呼叫 D::mf,行為一致。

理論推導:公繼承、非虛擬函式與介面契約

  1. 公繼承 = is-a 關係:所有 D 物件都是 B 物件 (條款 32)
  2. 非虛擬函式 = 強制繼承介面與實作:衍生類別必須繼承基底類別的介面**與**實作 (條款 34)
  3. 若 D 重新定義非虛擬函式 mf,產生矛盾:
    • 若 D 需不同實作,則非虛擬函式的「強制實作繼承」失效 → 應改為虛擬函式
    • 若 D 確實是 B,且 B 的實作適用所有衍生類,則 D **不應** 重新定義

無論從哪個角度,**必須無條件禁止重新定義繼承而來的非虛擬函式**。

特殊情況:解構函式 (條款 7)

多型基類的解構函式**必須為虛擬**。若基類解構函式非虛擬,刪除基類指標指向的衍生物件時,衍生類解構函式不會被呼叫。這本質上是條款 36 的特例:編譯器自動產生的解構函式屬於非虛擬函式,衍生類會隱含重新定義。

銘記於心

  • ✅ **避免在衍生類別中重新定義非虛擬函式**
  • ✅ 虛擬函式採動態繫結,預設參數採靜態繫結 —— **不要給虛擬函式預設參數**
  • ✅ 非虛擬函式代表「不變的介面契約」,衍生類別不得修改實作
  • ✅ 多型基類的解構函式必須宣告為 virtual
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved