核心原則

Item 32 的金句:Public Inheritance means "is-a" (公有繼承意味着“是一個”).

它的嚴格定義是:

如果類 D (Derived) 公有繼承自類 B (Base),那麼每一個類型為 D 的對象同時也是一個類型為 B 的對象。 任何需要 B 類型對象的地方(函數參數、指針等),如果你把 D 類型的對象傳進去,程序都必須表現正常。

這在軟件工程領域對應着著名的 里氏替換原則 (Liskov Substitution Principle, LSP)


經典陷阱 1:企鵝與鳥 (The Bird-Penguin Problem)

這是最容易犯錯的邏輯陷阱:生活常識 vs. 編程語義

直覺邏輯:

  1. 企鵝是鳥。
  2. 鳥會飛。
  3. 所以,企鵝應該繼承自鳥,並擁有“飛”的功能。

錯誤代碼:

class Bird {
public:
    virtual void fly(); // 鳥會飛
};

class Penguin : public Bird {
    // 繼承了 fly() 接口
};

void migrate(Bird& b) {
    b.fly(); // 所有的鳥都能飛?
}

Penguin p;
migrate(p); // 邏輯錯誤:企鵝飛起來了!或者你在運行時報錯?

問題分析: 在 C++ 中,公有繼承主張的是:所有對 Base 有效的事情,對 Derived 也必須有效。 如果 Bird 定義了 fly(),就是在向全世界承諾:“所有的鳥都會飛”。既然企鵝不會飛,那麼企鵝就不是(C++ 語義下的)鳥

修正方案: 區分“行為”,而不是單純區分“物種”。

class Bird {
    // Bird 的通用屬性
};

class FlyingBird : public Bird {
public:
    virtual void fly();
};

class Penguin : public Bird {
    // 企鵝是鳥,但不是 FlyingBird
};

這樣,如果函數參數要求 FlyingBird,你傳 Penguin 進去,編譯器就會直接報錯(Compile-time error),這比運行時錯誤好得多。


經典陷阱 2:矩形與正方形 (The Rectangle-Square Problem)

這個例子更加隱蔽,經常出現在數學背景較強的程序員代碼中。

直覺邏輯: 幾何學上,正方形(Square)是一個特殊的矩形(Rectangle)。所以 Square 應該公有繼承自 Rectangle

錯誤代碼:

class Rectangle {
public:
    virtual void setHeight(int newH);
    virtual void setWidth(int newW);
    virtual int height() const;
    virtual int width() const;
};

void makeBigger(Rectangle& r) {
    int oldHeight = r.height();
    
    // 這裏的邏輯對於矩形是完全正確的:
    // 改變寬度不應該影響高度。
    r.setWidth(r.width() + 10); 
    
    assert(r.height() == oldHeight); // 檢查高度是否未變
}

class Square : public Rectangle {
    // 正方形的特性:寬必須等於高
    // 所以我們需要重寫 setWidth 和 setHeight
    // 無論改哪個,都要同時修改兩邊
};

Square s;
s.setWidth(10); // 假設此時 h=10, w=10
makeBigger(s);  // 災難發生!

災難分析:

  1. makeBigger 接收一個 Rectangle 引用。
  2. 它調用 setWidth,心裏默認 Rectangle 的高度不會變(這是矩形的不變性 Invariant)。
  3. 但如果你傳進去的是 SquareSquare::setWidth 為了維持正方形的特性,被迫同時也修改了高度。
  4. assert 失敗,程序崩潰。

結論: 雖然在幾何學上正方形是矩形,但在 C++ 的公有繼承語義下,正方形並不是矩形。 因為矩形允許寬和高獨立變化,而正方形不允許。它們的行為契約不同。


如何判斷“Is-a”是否成立?

Scott Meyers 建議我們在設計繼承體系時,必須通過以下測試:

“Is-a” 關係必須滿足:Derived class 必須能無條件地替代 Base class,且不破壞程序的正確性。

如果在你的設計中:

  1. 代碼需要進行類型檢查 (Type Checking) 才能正常工作(例如 if (dynamic_cast<Penguin*>(&bird))),説明違反了 Item 32。
  2. 派生類屏蔽了基類的某些功能(比如把基類的 fly() 在子類中定義為報錯或空操作),説明這不是一個完美的 is-a 關係。

怎麼判斷你是對的?(靈魂三問)

寫代碼的時候,當你想要讓 B 繼承 A 時,問自己三個問題:

  1. A 的所有函數,B 都能調用嗎?
  2. A 調用的結果是正常的,B 調用的結果也必須是正常的嗎?
  3. 有沒有任何一個場景,用 A 沒問題,換成 B 就出 Bug?

如果有任何一個答案是“不對勁”,那就千萬別用公有繼承

其他關係 (如果不是 Is-a 怎麼辦?)

如果兩個類很像,但又不滿足完全的 is-a,你應該考慮:

  1. Has-a (有一個): 組合 (Composition)。例如,正方形“有一個”矩形作為實現細節,或者正方形和矩形都繼承自一個更抽象的 Shape 類。這對應 Item 38
  2. Is-implemented-in-terms-of (根據...實現): 私有繼承 (Private Inheritance)。這對應 Item 39

總結

  • Public 繼承 = Is-a (是一個)。
  • 這不僅是分類學上的歸屬,更是行為上的承諾
  • 基類做出的所有承諾(接口行為、不變性),派生類都必須遵守。
  • 如果發現派生類在某些行為上必須違背基類的邏輯,那麼請斷開繼承關係