物件導向三大支柱之二:繼承 (Inheritance)多型 (Polymorphism)。 繼承讓我們在既有類別的基礎上擴充功能;多型讓同一介面對不同型別產生不同行為。 這兩個機制是 C++ 設計大型可擴充系統的核心,但也是初學者最容易誤用的地方。


學習目標

讀完本章,你應該能夠:

  1. 理解繼承的語法與三種存取修飾(public / protected / private 繼承)各自的語意
  2. 區分「is-a(是一種)」、「has-a(擁有)」、「implemented-in-terms-of(以…實作)」三種關係
  3. 掌握基礎類別與衍生類別之間建構子/解構子的呼叫順序基礎類別初始化
  4. 區分「覆寫 (override)」與「隱藏 (hiding)」、「靜態綁定」與「動態綁定」的差異
  5. 解釋虛擬函式 (virtual function) 背後的 vtable / vptr 機制(能畫出記憶體圖)
  6. 說明為何需要虛擬解構子 (virtual destructor),並重現「忘記它」造成的 bug
  7. 使用 overridefinal 關鍵字提高安全性
  8. 定義純虛擬函式 (pure virtual function)、抽象類別 (abstract class) 與 C++ 的介面慣例
  9. 使用 dynamic_casttypeid 進行執行期型別識別 (RTTI),並知道何時不該
  10. 辨識並避免物件切割 (object slicing) 問題
  11. 處理多重繼承 (multiple inheritance) 與菱形繼承問題 (diamond problem),使用虛擬繼承
  12. 套用 C++ Core Guidelines 的「優先使用組合而非繼承」原則

0. 為什麼需要繼承與多型?

想像你要寫一個繪圖程式,需要處理圓形、長方形、三角形。沒有多型時,你會這樣寫:

void drawAll(const std::vector<Shape>& shapes) {
    for (const auto& s : shapes) {
        if (s.type == CIRCLE)         drawCircle(s);
        else if (s.type == RECTANGLE) drawRectangle(s);
        else if (s.type == TRIANGLE)  drawTriangle(s);
        // 每新增一種圖形,這裡、計算面積、存檔…全部都要改一遍
    }
}

這種「型別標籤 + 一長串 if/switch」的寫法有兩個致命缺點:

  1. 不可擴充 (Open-Closed 原則被破壞):每加一種圖形,所有 switch 都要修改。
  2. 容易遺漏:新增圖形卻忘了改某個 switch,編譯不會報錯,執行期才爆炸。

有了繼承與多型後:

void drawAll(const std::vector<std::unique_ptr<Shape>>& shapes) {
    for (const auto& s : shapes) {
        s->draw();   // 每個物件自己知道怎麼畫,新增圖形不必改這裡
    }
}

核心價值:繼承讓你重用與擴充既有程式碼;多型讓你對抽象介面寫程式,而不是對具體型別。


1. 繼承基礎

1.1 語法與術語

class Base {            // 基礎類別 (base / parent / superclass)
public:
    int x = 10;
    void hello() { std::cout << "Base::hello()\n"; }
};

class Derived : public Base {   // 衍生類別 (derived / child / subclass)
    int y = 20;
};
  • Derived 繼承了 Base 的所有非 private 成員與方法(private 成員仍存在,但不可直接存取)。
  • 繼承時的 public繼承存取修飾符,決定基礎類別成員被繼承後的可見性。

1.2 三種繼承存取修飾符的語意

這張表是理解繼承存取控制的核心:

基礎類別成員 public 繼承 protected 繼承 private 繼承
public public protected private
protected protected protected private
private 不可存取 不可存取 不可存取

但比「可見性表格」更重要的是它們代表的關係

繼承方式 代表關係 何時使用 範例
public is-a(是一種) 99% 的情況 Dog is-a Animal
private implemented-in-terms-of(以…實作) 少數,通常可用組合取代 StackVector 實作
protected 罕見 幾乎不用
class Animal { public: void breathe(); };

class Dog : public Animal {};      // Dog 是一種 Animal → 外部可把 Dog 當 Animal 用
class Engine {};
class Car : private Engine {};     // Car 不是 Engine,只是「用」Engine 實作 → 外部看不到 Engine 介面

實務原則:絕大多數情況使用 public 繼承。如果你想寫 private/protected 繼承,先問自己:「用組合(把它當成員)是不是更好?」答案通常是「對」。

1.3 is-a 關係與 Liskov 替換原則 (LSP)

public 繼承代表「is-a」,意思是任何接受基礎類別的地方都能安全地傳入衍生類別。這稱為 Liskov 替換原則

void feed(Animal& a) { a.eat(); }

Dog d;
feed(d);   // ✓ Dog is-a Animal,可以替換

反例(違反 is-a,是常見的設計錯誤)

class Rectangle {
public:
    virtual void setWidth(int w);
    virtual void setHeight(int h);
};

class Square : public Rectangle {   // ✗ 數學上 Square is-a Rectangle,但行為上不是!
    // setWidth 必須同時改 height,否則破壞「正方形」不變式
    // → 任何「設定寬高後驗證面積」的程式碼會被 Square 弄壞
};

「正方形繼承長方形」是經典的 LSP 反例:概念上的 is-a 不等於行為上的 is-a。設計繼承時要看行為,不只看名詞。

1.4 建構子與解構子呼叫順序

class Animal {
public:
    Animal()  { std::cout << "Animal 建構\n"; }
    ~Animal() { std::cout << "Animal 解構\n"; }
};

class Dog : public Animal {
public:
    Dog()  { std::cout << "Dog 建構\n"; }
    ~Dog() { std::cout << "Dog 解構\n"; }
};

int main() {
    Dog d;
}

預期輸出:

Animal 建構
Dog 建構
Dog 解構
Animal 解構

規則(務必記住這個對稱性):

  • 建構:基礎類別 → 衍生類別(由內而外、由上往下)。因為衍生類別可能用到基礎類別的成員,所以基礎類別必須先準備好。
  • 解構:衍生類別 → 基礎類別(由外而內、由下往上)。與建構順序相反,像堆疊一樣後進先出。
建構:  [Animal] → [Dog] → [GuideDog]      (往下蓋房子,先打地基)
解構:  [GuideDog] → [Dog] → [Animal]      (往上拆房子,先拆屋頂)

1.5 在衍生類別建構子中初始化基礎類別

基礎類別的初始化必須寫在初始化列表,因為它在衍生類別本體執行前就要完成。

class Animal {
public:
    explicit Animal(const std::string& name) : name_(name) {}
protected:
    std::string name_;
};

class Dog : public Animal {
public:
    Dog(const std::string& name, const std::string& breed)
        : Animal(name),     // ← 呼叫基礎類別建構子(必須在最前面)
          breed_(breed) {}
private:
    std::string breed_;
};

如果你不顯式呼叫,編譯器會嘗試呼叫基礎類別的預設建構子;若基礎類別沒有預設建構子,就會編譯錯誤:

class Dog : public Animal {
public:
    Dog() {}   // ✗ 錯誤:Animal 沒有預設建構子,又沒在初始化列表呼叫 Animal(...)
};

2. 方法覆寫、隱藏,與靜態 vs 動態綁定

這是初學者最容易混淆的地方,請務必區分三個詞。

2.1 靜態綁定 — 沒有 virtual 的「隱藏」

class Base {
public:
    void speak() { std::cout << "Base speaks\n"; }
};

class Derived : public Base {
public:
    void speak() { std::cout << "Derived speaks\n"; }   // 這是「隱藏」,不是覆寫
};

int main() {
    Derived d;
    Base* ptr = &d;
    ptr->speak();      // 輸出 "Base speaks" — 依「指標型別」決定!
    d.speak();         // 輸出 "Derived speaks"
}

預期輸出:

Base speaks
Derived speaks

沒有 virtual 時,編譯器在編譯期就依「指標/參考的靜態型別」決定呼叫哪個函式,這叫靜態綁定 (static binding / early binding)

2.2 動態綁定 — 使用 virtual 的「覆寫」

class Base {
public:
    virtual void speak() { std::cout << "Base speaks\n"; }
};

class Derived : public Base {
public:
    void speak() override { std::cout << "Derived speaks\n"; }
};

int main() {
    Derived d;
    Base* ptr = &d;
    ptr->speak();      // 輸出 "Derived speaks" — 依「物件實際型別」決定!
}

預期輸出:

Derived speaks

有了 virtual,呼叫哪個函式在執行期才依「物件的實際型別」決定,這叫動態綁定 (dynamic binding / late binding),是執行期多型的本質。

2.3 三者對照表

名詞 是否 virtual 綁定時機 依據 結果
隱藏 (hiding) 編譯期 指標/參考型別 呼叫基礎版本
覆寫 (override) 執行期 物件實際型別 呼叫衍生版本(多型)
多載 (overload) 編譯期 參數型別 同名不同參數

⚠️ 注意:衍生類別只要定義了同名函式,就會隱藏基礎類別所有同名多載(連參數不同的也一起藏)。需要時用 using Base::speak; 把它們帶回來。


3. 虛擬函式與 vtable / vptr 機制

3.1 編譯器如何實現動態綁定?

當類別含有虛擬函式時,編譯器會:

  1. 每個類別產生一張虛擬函式表 (virtual table, vtable),裡面是該類別各虛擬函式的實際位址。
  2. 每個物件裡塞一個隱藏指標 vptr (virtual pointer),指向所屬類別的 vtable。

呼叫 ptr->speak() 時,實際發生的是:ptr → 取出物件的 vptr → 找到 vtable → 查出 speak 的位址 → 呼叫。這就是為什麼能在執行期分辨實際型別。

3.2 記憶體佈局 ASCII 圖

     Base* p = new Derived();

     p ─────────────┐
                    ▼
        ┌────────────────────────┐         ┌──────────────────────────┐
        │  Derived 物件           │         │  Derived 的 vtable        │
        │ ┌────────────────────┐ │         ├──────────────────────────┤
        │ │ vptr  ──────────────┼─┼────────▶│ &Derived::speak()        │
        │ ├────────────────────┤ │         │ &Base::greet()  (未覆寫) │
        │ │ Base 的資料成員     │ │         │ &Derived::~Derived()     │
        │ ├────────────────────┤ │         └──────────────────────────┘
        │ │ Derived 的資料成員  │ │
        │ └────────────────────┘ │
        └────────────────────────┘
  • 同一個類別的所有物件共用一張 vtable(vtable 屬於類別,不屬於物件)。
  • 每個物件各自有一個 vptr(所以含虛擬函式的物件會多 sizeof(指標) 的大小)。

3.3 驗證 vptr 的大小開銷

class Empty {};
class WithVirtual { public: virtual void f() {} };

std::cout << sizeof(Empty) << "\n";        // 1(空類別至少 1 byte)
std::cout << sizeof(WithVirtual) << "\n";  // 8(64 位元系統:一個 vptr)

預期輸出(64 位元):

1
8

多型不是免費的:每個物件多一個指標、每次虛擬呼叫多一次間接跳轉。但對絕大多數程式而言,這點成本完全值得換來的彈性。


4. 虛擬解構子 — 為什麼這麼重要?

4.1 忘記虛擬解構子的 bug(請務必理解這個例子)

class Base {
public:
    Base()  { std::cout << "Base()\n"; }
    ~Base() { std::cout << "~Base()\n"; }   // ← 沒有 virtual!
};

class Derived : public Base {
    int* data_;
public:
    Derived() : data_(new int[100]) { std::cout << "Derived()\n"; }
    ~Derived() {
        delete[] data_;                       // 釋放資源的關鍵程式碼
        std::cout << "~Derived()\n";
    }
};

int main() {
    Base* p = new Derived();
    delete p;        // 💥 只呼叫 ~Base()!~Derived() 被跳過!
}

預期輸出(錯誤行為):

Base()
Derived()
~Base()

~Derived() 沒有被呼叫data_ 的 100 個 int 永久洩漏!更糟的是,這在 C++ 標準裡是未定義行為 (undefined behavior)

4.2 修正:加上 virtual

class Base {
public:
    virtual ~Base() { std::cout << "~Base()\n"; }   // ← 加上 virtual
};

修正後的輸出(正確):

Base()
Derived()
~Derived()
~Base()

4.3 規則

C++ Core Guidelines C.35A base class destructor should be either public and virtual, or protected and non-virtual.

翻譯:只要一個類別會被當作多型基礎類別(透過基礎指標 delete 衍生物件),它的解構子就必須是 public virtual。如果不希望被多型刪除,則設為 protected non-virtual

class Base {
public:
    virtual ~Base() = default;   // 最常見、最安全的寫法
};

5. overridefinal

5.1 override — 讓編譯器幫你抓錯

class Base {
public:
    virtual void draw() const;
};

class Derived : public Base {
public:
    void draw() override;          // ✓ 明確標記覆寫
    // void draw();                // 漏了 const → 變成「隱藏」而非覆寫,但編譯器不會警告
    // void draw() override;       // ✗ 加了 override 後,簽名不符會立刻編譯錯誤!
};

override 不改變行為,但會讓編譯器檢查你是否真的覆寫了基礎類別的虛擬函式。拼錯名字、漏寫 const、參數型別不符——都會在編譯期被抓到。

C++ Core Guidelines C.128:虛擬函式應該恰好標記 virtualoverridefinal 三者之一。衍生類別的覆寫一律用 override(不要重複寫 virtual)。

5.2 final — 禁止進一步繼承或覆寫

class Shape {
public:
    virtual double area() const = 0;
    virtual void draw() const;
};

class Circle final : public Shape {       // final:Circle 不能再被繼承
public:
    double area() const override;
    void draw() const final;               // final:子類別不能再覆寫 draw(但 Circle 已 final,此處示意)
};

// class Ball : public Circle {};          // ✗ 編譯錯誤:Circle 是 final

final 的用途:表達設計意圖(這個類別不該被擴充)、並讓編譯器有機會做去虛擬化 (devirtualization) 最佳化。


6. 純虛擬函式、抽象類別與介面

6.1 純虛擬函式與抽象類別

class Shape {
public:
    virtual double area() const = 0;     // 純虛擬函式(= 0):只有宣告,沒有定義
    virtual void draw() const = 0;
    virtual ~Shape() = default;          // 別忘了虛擬解構子
};

// Shape s;   // ✗ 編譯錯誤:抽象類別無法實例化
  • 含有至少一個純虛擬函式的類別即為抽象類別 (abstract class),無法直接建立物件。
  • 衍生類別必須實作所有純虛擬函式,否則它自己也是抽象類別。
  • 抽象類別的作用是定義介面契約:「凡是 Shape,都保證能 area() 和 draw()」。

6.2 C++ 的「介面 (interface)」慣例

C++ 沒有 Java/C# 的 interface 關鍵字。慣例上,一個只有純虛擬函式、沒有資料成員的抽象類別就扮演介面的角色:

class Drawable {                      // 純介面
public:
    virtual void draw() const = 0;
    virtual ~Drawable() = default;
};

class Serializable {                  // 另一個純介面
public:
    virtual std::string serialize() const = 0;
    virtual ~Serializable() = default;
};

// 一個類別可以實作多個介面(這是多重繼承的「好」用法)
class Widget : public Drawable, public Serializable {
public:
    void draw() const override { /* ... */ }
    std::string serialize() const override { /* ... */ }
};

C++ Core Guidelines I.25 / C.121:用抽象類別表達介面;介面類別通常沒有資料成員、沒有建構子,只有純虛擬函式與虛擬解構子。


7. 物件切割 (Object Slicing) — 重要陷阱!

這是繼承中最隱蔽的 bug 之一。當你把衍生類別物件以值 (by value) 指派或傳遞給基礎類別型別時,衍生部分會被「切掉」。

class Base {
public:
    virtual void who() const { std::cout << "Base\n"; }
};

class Derived : public Base {
    int extra_ = 42;
public:
    void who() const override { std::cout << "Derived\n"; }
};

void byValue(Base b)   { b.who(); }   // ✗ 以值傳遞 → 切割
void byRef(const Base& b) { b.who(); }// ✓ 以參考傳遞 → 多型正常

int main() {
    Derived d;

    byValue(d);   // 輸出 "Base"   —— d 被切割成只剩 Base 部分!
    byRef(d);     // 輸出 "Derived" —— 正確的多型

    Base b = d;   // 切割:b 只複製了 d 的 Base 部分,extra_ 遺失
    b.who();      // 輸出 "Base"
}

預期輸出:

Base
Derived
Base
     Derived d                         Base b = d;  (切割!)
   ┌──────────────┐                   ┌──────────────┐
   │ vptr→Derived │   ─── 複製 ──▶    │ vptr→Base    │   ← vptr 變回 Base
   │ Base 部分    │                   │ Base 部分    │
   │ extra_ = 42  │   ✂ 被切掉        └──────────────┘
   └──────────────┘

如何避免切割:操作多型物件時,永遠使用基礎類別的指標或參考Base*Base&std::unique_ptr<Base>),絕不用值。


8. 執行期型別識別 (RTTI)

8.1 dynamic_cast — 安全的向下轉型

Base* base = getSomeShape();

if (Circle* c = dynamic_cast<Circle*>(base)) {   // 轉型成功才進入
    c->setRadius(5.0);                           // 安全地使用 Circle 專屬功能
}
  • 指標版本:轉型失敗回傳 nullptr(所以可以用 if 檢查)。
  • 參考版本:轉型失敗拋出 std::bad_cast(參考不能是 null)。
  • 需要基礎類別至少有一個虛擬函式(具備 RTTI 資訊)才能用。

8.2 typeid — 取得執行期型別資訊

#include <typeinfo>

Base* p = new Derived();
std::cout << typeid(*p).name() << "\n";   // 輸出實際型別(名稱經過 mangling,因編譯器而異)

if (typeid(*p) == typeid(Derived)) {
    std::cout << "p 確實指向 Derived\n";
}

⚠️ typeid(*p) 要解參考才會啟用多型查詢;typeid(p) 只會得到指標的靜態型別 Base*

8.3 何時該用、何時不該用 RTTI

情境 建議
偶爾需要「恢復」具體型別做特殊處理 可用 dynamic_cast
撰寫安全的向下轉型 dynamic_cast(勝過 C-style cast)
一長串 if (dynamic_cast<A>) … else if (dynamic_cast<B>) 設計有問題,應改用虛擬函式
效能極度敏感的迴圈 避免,dynamic_cast 有執行期成本

大量使用 dynamic_cast 通常是「多型沒設計好」的訊號。先問:能不能把這段邏輯變成基礎類別的一個虛擬函式?


9. 多重繼承與菱形問題

9.1 多重繼承

class Flyable    { public: virtual void fly()  { std::cout << "Flying\n"; } virtual ~Flyable() = default; };
class Swimmable  { public: virtual void swim() { std::cout << "Swimming\n"; } virtual ~Swimmable() = default; };

class Duck : public Flyable, public Swimmable {   // 同時是 Flyable 與 Swimmable
};

多重繼承最安全的用法是「繼承多個純介面」(如上)。繼承多個有實作、有資料的類別則容易出問題。

9.2 菱形繼承問題 (Diamond Problem)

        Animal              ← 含有 name_ 成員
       /      \
     Dog      Cat           ← 兩者都繼承 Animal
       \      /
       Hybrid              ← 💥 含有「兩份」Animal!
class Animal { public: std::string name_; };
class Dog : public Animal {};
class Cat : public Animal {};
class Hybrid : public Dog, public Cat {};

Hybrid h;
// h.name_ = "x";        // ✗ 模稜兩可!是 Dog::Animal::name_ 還是 Cat::Animal::name_?
h.Dog::name_ = "x";      // 必須明確指定,而且有兩份資料,浪費又混亂

9.3 解法:虛擬繼承 (virtual inheritance)

class Animal { public: std::string name_; };
class Dog : virtual public Animal {};   // ← virtual
class Cat : virtual public Animal {};   // ← virtual
class Hybrid : public Dog, public Cat {};

Hybrid h;
h.name_ = "x";   // ✓ 只有一份 Animal,不再模稜兩可
   虛擬繼承後:
        Animal              ← 只有「一份」共享
       /      \
     Dog      Cat
       \      /
       Hybrid

建議:菱形繼承很複雜(建構子初始化規則、效能開銷都更麻煩)。除非在實作介面,否則盡量避免多重繼承實作類別


10. 最佳實踐:優先使用組合而非繼承

這是物件導向設計最重要的原則之一。

10.1 繼承被濫用的徵兆

// ✗ 不良設計:只是想重用 vector 的功能就去繼承它
class Stack : public std::vector<int> {   // Stack 並不「是一種」vector
public:
    void push(int x) { push_back(x); }
    // 但使用者還能呼叫 stack.insert()、stack[5] … 破壞了 Stack 的封裝!
};

10.2 改用組合 (composition)

// ✓ 良好設計:Stack「擁有 (has-a)」一個 vector
class Stack {
    std::vector<int> data_;          // 組合:把 vector 當成員
public:
    void push(int x) { data_.push_back(x); }
    void pop()       { data_.pop_back(); }
    int  top() const { return data_.back(); }
    bool empty() const { return data_.empty(); }
    // 只暴露 Stack 該有的介面,完全控制
};

10.3 如何選擇?

問題 答案 → 用
「B 是一種 A 嗎?」(行為上可替換) 是 → 繼承 (public)
「B 擁有/使用 A 嗎?」 是 → 組合(成員)
只是想重用 A 的程式碼? 組合(不要為了重用而繼承)
需要多型(用基礎指標統一操作)? 繼承 + 虛擬函式

C++ Core Guidelines C.129 / C.121If a base class is used as an interface, make it a pure abstract class. 並且設計類別階層時「優先組合,謹慎繼承」。繼承會建立強耦合,組合則保持彈性。


重點整理

概念 說明
public 繼承 is-a 關係,最常用,須符合 Liskov 替換
private 繼承 implemented-in-terms-of,通常可用組合取代
建構順序 基礎 → 衍生;解構順序相反
基礎類別初始化 寫在初始化列表,必須最先
靜態綁定 無 virtual,依指標型別,編譯期決定
動態綁定 有 virtual,依物件實際型別,執行期決定
vtable / vptr 每類別一張表,每物件一個指標,實現多型
虛擬解構子 多型基礎類別必須 public virtual ~T()
override 讓編譯器檢查是否真的覆寫
final 禁止繼承或覆寫
純虛擬函式 (=0) 定義抽象類別 / 介面
物件切割 以值操作多型物件會切掉衍生部分;用指標/參考避免
dynamic_cast 安全向下轉型,失敗回 nullptr / 拋例外
虛擬繼承 解決菱形繼承的重複基礎問題
組合 vs 繼承 優先組合;只有 is-a + 需要多型才繼承

常見錯誤與陷阱

  1. 忘記虛擬解構子:透過基礎類別指標 delete 衍生物件時,衍生解構子被跳過,造成資源洩漏與未定義行為。規則:多型基礎類別 → virtual ~Base() = default;

  2. 覆寫時函式簽名不一致:參數型別、const、回傳型別不同會變成「隱藏」而非「覆寫」,多型失效。永遠加 override,讓編譯器提前抓到。

  3. 物件切割:以值傳遞/指派多型物件(void f(Base b)Base b = derived;)會切掉衍生部分。操作多型物件一律用指標或參考。

  4. 在建構子或解構子中呼叫虛擬函式:此時 vtable 指向「當前正在建構/解構的類別」,呼叫的不是最終衍生版本。這幾乎總是 bug。

  5. 名稱隱藏:衍生類別定義同名函式會隱藏基礎類別所有同名多載。需要時用 using Base::func; 帶回。

  6. 誤用 private 繼承表達 is-a:private 繼承是「實作細節」,外部不能把它當基礎類別用。想要 is-a 請用 public 繼承。

  7. 濫用 dynamic_cast 取代多型:一長串 if (dynamic_cast<...>) 是設計警訊,通常該改成虛擬函式。

  8. 為了重用程式碼而繼承:「想用某類別的功能」不是繼承的理由,那是組合的工作。繼承會帶來強耦合與脆弱基礎類別問題。

  9. 多重繼承實作類別導致菱形問題:除非繼承純介面,否則小心多重繼承;需要共享基礎時用虛擬繼承。

  10. typeid(p) 寫錯:要查實際型別必須 typeid(*p)(解參考);typeid(p) 只得到指標靜態型別。


練習題

以下練習由淺入深,建議全部動手實作。每題附「提示」。

練習 1(基礎)— 員工階層

設計 Person 基礎類別(name_age_),衍生 Student(加上 studentId_grade_)與 Teacher(加上 subject_years_)。各自實作 introduce() 印出個人資訊。

  • 提示introduce() 設為 virtual;衍生類別用 override;在衍生的 introduce() 中可呼叫 Person::introduce() 重用基礎部分。

練習 2(基礎)— 建構解構順序觀察

建立三層繼承 A → B → C,每個類別的建構子與解構子都印出訊息。在 main 中建立一個 C 物件並觀察輸出,再用 A* p = new C(); delete p; 觀察「有無虛擬解構子」的差異。

  • 提示:先讓 ~A() 非虛擬,記錄輸出;再改成 virtual ~A(),比較兩次結果。

練習 3(中級)— 圖形多型與總面積

設計抽象類別 Shape,含純虛擬函式 area()perimeter()。衍生 CircleRectangleTriangle。用 std::vector<std::unique_ptr<Shape>> 裝入各種圖形,計算總面積,並找出面積最大的圖形。

  • 提示:別忘了 virtual ~Shape() = default;;用 std::make_unique 建立物件;遍歷時用 s->area()

練習 4(中級)— 物件切割重現

寫一個函式 print(Shape s)(以值傳遞)與 print(const Shape& s)(以參考傳遞),各自呼叫虛擬函式 draw()。傳入一個 Circle,觀察兩者輸出不同,並用一段文字說明為什麼。

  • 提示:以值傳遞時 vptr 被重設為 Shape,輸出基礎版本;以參考則維持多型。

練習 5(中級)— 介面與 RTTI

設計兩個純介面 Printableprint())與 Serializableserialize())。讓 Document 同時實作兩者。建立 std::vector<Printable*>,其中混入同時為 Serializable 的物件,用 dynamic_cast 找出可序列化的物件並呼叫 serialize()

  • 提示:介面只放純虛擬函式 + 虛擬解構子;dynamic_cast<Serializable*>(ptr) 成功才呼叫。

練習 6(挑戰)— 組合重構

給定一個「為了重用而繼承」的不良設計 class Logger : public std::vector<std::string>,把它重構成使用組合Logger(內部持有 std::vector<std::string>),只暴露 log(msg)dump()clear() 三個方法。寫一段說明,比較重構前後在封裝性上的差異。

  • 提示:把 vector 變成 private 成員;對照 §10「組合 vs 繼承」表格論述。

練習 7(挑戰)— 菱形繼承與虛擬繼承

設計 Device(含 serialNo_)為共同基礎,ScannerPrinter 繼承自它,MultiFunctionPrinter 同時繼承 ScannerPrinter。先不用虛擬繼承,觀察 serialNo_ 的存取為何模稜兩可;再改成虛擬繼承解決,並驗證只剩一份 serialNo_

  • 提示class Scanner : virtual public Device;注意虛擬繼承時,最終衍生類別 MultiFunctionPrinter 要負責呼叫 Device 的建構子。

對應程式碼檔案

檔案 說明
inheritance.cpp 繼承基礎:建構/解構順序、protected 成員、方法隱藏、多層繼承
polymorphism.cpp 虛擬函式、多型、虛擬解構子對比、override、vtable 大小驗證
abstract_class.cpp 純虛擬函式、抽象類別、介面、dynamic_cast、typeid、final