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

Effective C++ 筆記:繼承與面向對象設計精要 (條款 32-40)

     編輯:C++ 入門

核心原則速覽

條款核心觀點關鍵詞
32public 繼承 = is-a 關系里氏替換原則
33避免遮掩繼承名稱using 聲明 / 轉交函數
34區分接口繼承與實現繼承pure virtual / impure virtual / non-virtual
35考慮 virtual 替代方案NVI / Strategy / Function Pointer
36絕不重新定義 non-virtual 函數靜態綁定 vs 動態綁定
37絕不重新定義繼承的缺省參數缺省參數靜態綁定
38復合表達 has-a / implemented-in-terms-of優先復合過繼承
39審慎使用 private 繼承訪問 protected / 空基優化
40審慎使用多重繼承虛繼承解決鑽石問題

條款 32:public 繼承塑模 is-a 關系

**定義**:`public` 繼承意味著「是一個」。基類契約 (介面、不變式、語義) 必須完整適用於派生類。

**反例**:`Penguin : public Bird`,若 `Bird::fly()` 存在,企鵝違反契約。正確建模應抽象出 `FlyingBird` 或用特徵類。

條款 33:避免遮掩繼承名稱

派生類同名函數會**遮掩**基類**所有**同名重載:

class Base { virtual void mf1(); virtual void mf1(int); void mf3(); void mf3(double); };
class Derived : public Base { virtual void mf1(); void mf3(); };

Derived d; d.mf1(10); // 錯誤!Derived::mf1 遮掩了 Base::mf1(int)

解決方案

  1. using 聲明:`using Base::mf1; using Base::mf3;` 引入基類重載集
  2. 轉交函數:private 繼承 + public 轉發,精確控制暴露介面

條款 34:區分接口繼承與實現繼承

函數類型接口繼承實現繼承
pure virtual✓✗ (強制派生類實現)
impure virtual✓✓ (提供缺省實現,可覆蓋)
non-virtual✓✓ (強制繼承實現,不可覆蓋)

條款 35:virtual 替代方案

1. NVI (Non-Virtual Interface) = Template Method 模式

class GameCharacter {
public:
    int healthValue() const {  // public non-virtual
        // 前置檢查、日誌、鎖...
        int ret = doHealthValue();  // 調用 private virtual
        // 後置處理...
        return ret;
    }
private:
    virtual int doHealthValue() const = 0;  // 派生類實現
};

**優勢**:基類控制調用前後邏輯,派生類只關注核心算法。

2. Strategy 模式

將算法封裝為可替換對象:

  • 函數指針:`int (*calc)(const Character&)`
  • `std::function` / `std::bind` (C++11):支援仿函數、成員函數、Lambda
  • 經典:策略類層次結構

**優勢**:運行時動態切換策略,同一對象不同階段可用不同算法。

條款 36:絕不重新定義 non-virtual 函數

non-virtual = **靜態綁定** (編譯期根據指針/引用類型決定);virtual = **動態綁定** (運行期根據對象類型決定)。

Base* pb = new Derived;
pb->nonVirtualFunc();  // 永遠調用 Base::nonVirtualFunc()!

**後果**:違反 is-a,導致行為不一致。

條款 37:絕不重新定義繼承的缺省參數

**缺省參數靜態綁定**,virtual 函數動態綁定 → **靜態類型決定缺省值,動態類型決定函數體**。

class Shape { virtual void draw(Color c = Red) = 0; };
class Rect : public Shape { void draw(Color c = Green) override; };

Shape* ps = new Rect;
ps->draw();  // 調用 Rect::draw(),但缺省參數是 Red!

解決方案:NVI 手法

class Shape {
public:
    void draw(Color c = Red) const { doDraw(c); }  // 缺省參數只在這裡
private:
    virtual void doDraw(Color c) const = 0;  // 無缺省參數
};

條款 38:復合優於繼承表達 has-a

**繼承 = is-a** (類型關系);**復合 = has-a / implemented-in-terms-of** (實現關系)。

**例**:`Set` 不應繼承 `List` (List 允許重複,Set 不允許,違反 is-a);應包含 `List` 成員並封裝去重邏輯。

條款 39:審慎使用 private 繼承

`private` 繼承 = **implemented-in-terms-of** (按某物實現)。適用場景:

  1. 需訪問基類 `protected` 成員
  2. 需重新定義基類 `virtual` 函數
  3. **空基類優化 (EBO)**:`class Derived : private Empty { int x; };` → `sizeof(Derived) == sizeof(int)`,無填充

**一般情況優先復合**,private 繼承耦合度更高。

條款 40:審慎使用多重繼承

鑽石繼承問題

class File { string name; };
class InputFile : public File { ... };
class OutputFile : public File { ... };
class IOFile : public InputFile, public OutputFile { ... }; // 兩份 name!

虛繼承解決

class InputFile : virtual public File { ... };
class OutputFile : virtual public File { ... };
class IOFile : public InputFile, public OutputFile { ... }; // 單份 name

代價

  • 對象佈局複雜,體積/速度開銷
  • 構造函數調用順序複雜 (虛基類由最派生類直接初始化)
  • **最實用場景**:虛基類**不帶數據**,純介面

正當用例

`public` 繼承介面類 + `private` 繼承實現輔助類 = **介面繼承 + 實現復用** 的組合。

總結:設計決策清單

  1. 是 **is-a** 嗎?→ `public` 繼承
  2. 只是復用代碼?→ 復合 / `private` 繼承
  3. 需運行時換算法?→ Strategy / `std::function`
  4. 基類控制流程、派生類填細節?→ NVI
  5. 多重繼承必要嗎?→ 虛繼承解鑽石,純介面虛基類
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved