iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0

之前就有學過物件導向程式設計(OOP)的「觀念」,但一直沒有認真看相關的程式碼,現在就來透過 C++ 程式碼,搞懂 OOP 是怎麼實現的。

類別 class 與實例 instance

「類別」是用來建立物件的模板,而從模板做出來的一個個新物件稱為「實例」。

以下列範例來說,main() 函式裡面以 Flashlight 類別為基礎,建立一個名為 myLight 的實例:

#include <iostream>

// Flashlight 類別
class Flashlight {
public:
    // 屬性(資料)
    bool isOn;

    // 方法(函數)
    void toggle() {
        isOn = !isOn;
        if (isOn) {
            std::cout << "Flashlight on\n";
        } else {
            std::cout << "Flashlight off\n";
        }
    }
};

int main() {
    // 以 Flashlight 類別為模板做的實例,命名為 myLight
    Flashlight myLight; 
    
    myLight.isOn = false; // 初始化 myLight 實例的屬性
    myLight.toggle();     // 呼叫 myLight 實例的方法
}

物件生命週期:建構子與解構子

當一個類別的物件被建立時,會自動呼叫「建構子(constructor)」函數,其特色包含:

  1. 功能:用來初始化資料成員(例如把記憶體空間配置好)。
  2. 長相:函數名稱跟類別一樣,且沒有回傳型別但可以有參數。

而當一類別的物件被銷毀時,會自動呼叫「解構子(destructor)」函數,其特色包含:

  1. 功能:用來清理善後(例如把記憶體空間還回去)。
  2. 長相:用波浪號 ~ 開頭,函數名稱也跟類別一樣,亦無回傳型別且沒有參數。

值得留意的是,如果直接宣告多於一個實例,這些物件實例會存在記憶體的「堆疊(stack)」中,而堆疊有「後進先出」特性,因此後建立的物件反而會先死亡。

上述說明可搭配下列程式碼觀察,當中的 privatepublic 會在下個段落說明:

#include <iostream>
#include <string>

using namespace std;

class Player {
private: // 下個段落說明 private 意義
    string name; // Player 類別的角色名字

public:  // 下個段落說明 public 意義
    // 建構子:名字和類別一樣都是 Player,負責在物件誕生時把名字設定好
    Player(string inputName) {
        name = inputName;
        cout << name << " is born!" << endl;
    }
    
    // 建構子的另一種更好的寫法:成員初始化列表(member initializer list)
    // 確保資料誕生就完成初始化
    // 而非先建立預設值後再賦值覆蓋
    // Player(string inputName) : name(inputName) {
    //     // 大括號內只需專心處理實際邏輯,不用管資料建立過程
    //     cout << name << " is born!" << endl;
    // }

    // 解構子:名字和類別一樣都是 Player,但前面加上 ~,負責在物件死亡時收尾
    ~Player() {
        cout << name << " is dead..." << endl;
    }
};

int main() {

    { // 大括號內為作用域域範圍(Scope)
        
        Player p1("Arthur"); // 第一個物件 p1 誕生,自動執行建構子
        Player p2("Anna"); // 第二個物件 p2 誕生,自動執行建構子
        
    } // 離開大括號,p2 物件的生命週期先結束,自動執行解構子
      // 接著, p1 物件的生命週期也結束,自動執行解構子

}

執行結果如下,可發現後建立的實例(名為 Annap2)反而先死亡:

Arthur is born!
Anna is born!
Anna is dead...
Arthur is dead...

封裝 Encapsulation

「封裝」共有兩個要點:

  1. 把資料(屬性)與操作該資料的函數(方法)都打包在同一類別中。
  2. 控制可存取之資料權限。

publicprivate

不論屬性或方法,只要寫在 public 區就表示在此類別外也可以取用、private 則表示僅限於同類別中使用。

以下列範例而言,只有類別內部可以動到 balance 屬性:

#include <iostream>

class BankAccount {
private:
    int balance = 0; // 只有 BankAccount 這個類別的內部函數可以存取 balance 變數

public:
    // 非 BankAccount 類別也可以操作 deposit() 函數存款
    // deposit() 函數也在同一類別中,因此可操控 balance
    void deposit(int amount) {
        if (amount > 0) {
            balance += amount;
            std::cout << "Deposit " << amount << " Made\n";
        } else {
            std::cout << "Balance should be at least 0\n";
        }
    }

    // 非 BankAccount 類別也可以操作 getBalance() 函數查詢餘額
    int getBalance() {
        return balance;
    }
};

int main() {
    BankAccount myAccount;  // 以 BankAccount 類別建立一個實例,實例名為 myAccount

    // main() 函數中不能直接存取 private 區塊的 balance 屬性
    // myAccount.balance = -1000; 

    //  main() 函數中可以存取 public 區塊的 deposit() 函數
    // deposit() 函數再默默調整 balance 的值
    myAccount.deposit(500);  // 成功存款:Deposit 500 Made
    myAccount.deposit(-100); // 錯誤:Balance should be at least 0
    
    //  main() 函數中可以存取 public 的 getBalance() 函數
    // 顯示 Current balance: 500 dollars
    std::cout << "Current balance: " << myAccount.getBalance() << " dollars\n";
}

執行結果如下:

Deposit 500 Made
Balance should be at least 0
Current balance: 500 dollars

繼承 Inheritance

剛剛的範例是以「銀行帳戶 BankAccount」為類別,現在要新增兩類別:

  • 「定存帳戶FixedDepositAccount
  • 「證券帳戶 SecuritiesAccount

這兩種帳戶也都要有 balance 變數跟 getBalance() 方法,但建立兩新類別時若都各自再定義 balancegetBalance() 顯得累贅,因此可以從 BankAccount 類別延伸而來。

首先,定存帳戶類別可以這樣定義:

class FixedDepositAccount : public BankAccount {  // 表示 FixedDepositAccount 類別繼承自 BankAccount 類別
public:
    void applyMonthlyInterest() {  // FixedDepositAccount 新類別自有方法
        int interest = balance * 0.05; 
        balance += interest; // 思考:可以直接操作 balance 變數嗎?
        std::cout << "[Fixed Deposit] Monthly interest of " << interest << " dollars has been credited.\n";
    }
};

證券帳戶類別可以這樣定義:

class SecuritiesAccount : public BankAccount {  // 表示 SecuritiesAccount 類別繼承自 BankAccount 類別
public:
    void buyStock(std::string stockCode, int cost) {  // SecuritiesAccount 新類別自有方法
        if (balance >= cost) {
            balance -= cost; // 思考:可以直接操作 balance 變數嗎?
            std::cout << "[Securities] Successfully purchased stock " << stockCode << ", deducted " << cost << " dollars.\n";
        } else {
            std::cout << "[Securities] Insufficient balance, unable to purchase stock!\n";
        }
    }
};

protected 區塊

在這兩個新類別中,都有直接操作 balance 變數,但當初為了不讓 balance 被任意改動而被放在 private 區,這樣這些新類別還能動 balance 嗎?

要讓變數或方法不輕易被其他地方改動,但又希望可以被繼承下來的新類別改動,還有另外一種稱為 protected 的區塊,因此可以把原本的 BankAccount 類別改寫如下:

class BankAccount {
protected:
    int balance = 0;  // 從 private 搬到 protected,讓繼承的新類別可以動、其他地方還是不能動

public:
    void deposit(int amount) {
        if (amount > 0) {
            balance += amount;
            std::cout << "Deposit " << amount << " Made\n";
        } else {
            std::cout << "Balance should be at least 0\n";
        }
    }

    int getBalance() {
        return balance;
    }
};

菱形問題 Diamond Problem

假設又有一個名為 WealthManagementAccount 的新類別要同時繼承剛剛的 FixedDepositAccountSecuritiesAccount,表示這個帳戶同時可以定存又可以買賣股票:

class WealthManagementAccount : public FixedDepositAccount, public SecuritiesAccount {
    // 同時擁有 applyMonthlyInterest() 和 buyStock() 方法
};

新類別同時繼承兩個舊類別是合法的,但問題在於這兩個中間類別又都繼承自同一個更上層的 BankAccount 類別,而 balance 變數就放在 BankAccount 類別中:

        [ BankAccount (擁有 balance) ]
             /                    \
  [ FixedDepositAccount ]    [ SecuritiesAccount ]
             \                    /
          [  WealthManagementAccount  ]

FixedDepositAccountSecuritiesAccount 兩類別的 balance 都是從 BankAccount 類別拷貝而來的。

這會造成如果要在 WealthManagementAccount 類別對 balance 進行操作,編譯器會不清楚到底要拿 FixedDepositAccount 那邊的 balance,還是 SecuritiesAccount 那邊的 balance,這就是「菱形問題(diamond problem)」。

                 [ BankAccount ]
                   balance 變數
             /                    \
  [ FixedDepositAccount ]    [ SecuritiesAccount ]
      balance 變數拷貝一        balance 變數拷貝二
             \                    /
          [  WealthManagementAccount  ]
         不知道 balance 要取拷貝一還是拷貝二

菱形問題解法:虛擬繼承 Virtual Inheritance

要解決菱形問題,可以用「虛擬繼承」,其核心觀念為讓所有新類別都共同指向同一塊屬於 BankAccount 類別 balance 變數的記憶體空間:

          [ BankAccount (擁有 balance) ]
                    balance 變數
               /           ^^^     \
  [ FixedDepositAccount ]  |||  [ SecuritiesAccount ]       
    指向 balance 變數指標 -─┘|└─- 指向 balance 變數指標
             \              |         /
                  指向 balance 變數指標
              [  WealthManagementAccount  ]

而寫法則是變成:

#include <iostream>
#include <string>

// // 最頂層:基礎類別
class BankAccount {
protected:
    int balance = 0;  // 下方虛擬繼承用的 balance 變數都是這個

public:
    void deposit(int amount) {
        if (amount > 0) {
            balance += amount;
            std::cout << "Deposit " << amount << " Made\n";
        }
    }
    int getBalance() {
        return balance;
    }
};

// 中間層:加上 virtual 關鍵字

// 虛擬繼承:balance 變數也來自最頂層的 BankAccount 類別
class FixedDepositAccount : virtual public BankAccount {
public:
    void applyMonthlyInterest() {
        int interest = balance * 0.05; 
        balance += interest;
        std::cout << "[Fixed Deposit] Monthly interest of " << interest << " dollars has been credited.\n";
    }
};

// 虛擬繼承:balance 變數也來自最頂層的 BankAccount 類別
class SecuritiesAccount : virtual public BankAccount {
public:
    void buyStock(std::string stockCode, int cost) {
        if (balance >= cost) {
            balance -= cost;
            std::cout << "[Securities] Successfully purchased stock " << stockCode << ", deducted " << cost << " dollars.\n";
        } else {
            std::cout << "Balance should be at least 0\n";
        }
    }
};

// 最底層:balance 變數也來自最頂層的 BankAccount 類別,但不用寫 virtual
class WealthManagementAccount : public FixedDepositAccount, public SecuritiesAccount {
public:
    // 這裡的 balance 也會是最頂層 BankAccount 類別中的 balance 變數
};

多型 Polymorphism

多型比較複雜一些,表示讓不同物件針對同樣的訊息有差異化的操作,包含:

  • 編譯時期(靜態,表示編譯時期就決定要呼叫哪個函數或運算子)
    • 函數多載(function overloading)
    • 運算子重載(operator overloading)
  • 執行時期(動態)
    • 函數覆寫(function overriding)

函數多載 Function Overloading

表示可以有同名、但參數的數量或種類不同。

BankAccount 類別而言,假設有兩種存款方式,一是存入現金、二是存入支票,就可以定義兩個函數,但名字皆為 deposit

class BankAccount {
public:
    // 版本 A:存入現金,參數為 int 型別
    void deposit(int amount) {
        if (amount > 0) {
            balance += amount;
            std::cout << "[Overload A] Cash Deposit of $" << amount << " Made.\n";
        }
    }

    // 版本 B:存入支票(字串)。函數同名,但參數變成 string
    void deposit(std::string checkNumber) {
        // 假設支票一律兌現 $10000
        balance += 10000;
        std::cout << "[Overload B] Check #" << checkNumber << " Processed. $10000 Deposited.\n";
    }
};

main() 函數以 BankAccount 類別開了一個名為 myAccount 的實例:

BankAccount myAccount;

那如果遇到 deposit() 函數時:

myAccount.deposit(500);   // 傳入整數型別,跑版本 A
myAccount.deposit("CH109238");  // 傳入字串型別,跑版本 B

在編譯時期,編譯器就會自動根據參數狀況判斷要跑哪種版本。

運算子重載 Operator Overloading

可定義如 +-* 等運算子如何在定義的物件上使用。

例如在 BankAccount 類別中,定義 + 運算子可讓兩個 BankAccount 類別相加:

BankAccount operator+(const BankAccount& other) {
    BankAccount combinedAccount;
    combinedAccount.balance = this->balance + other.balance;
    return combinedAccount;
}

首先第一行:

BankAccount operator+(const BankAccount& other)

分別表示:

  • BankAccount:表示算完之後會回傳一個 BankAccount 物件。
  • operator+:表示現在要定義 + 這個運算子的行為。
  • (const BankAccount& other):參數名稱為 other、型別為 BankAccount 且用參考傳遞而非傳整個物件,前面加上 const 保證不會改到傳進來的 other

再來的:

BankAccount combinedAccount;

表示宣告一個型別為 BankAccount 的變數 combinedAccount

combinedAccount.balance = this->balance + other.balance;

則是說 combinedAccountbalance 變數會是目前這個物件自己的餘額 this->balance 跟傳進來的 other 物件的餘額 other.balance 相加。

main() 函數中,以 BankAccount 為模板創建兩個實例,實例名稱分別為 acc1acc2,先取用這兩個實例的 deposit() 方法分別存入 300 元、200 元:

BankAccount acc1;   // 第一個實例
BankAccount acc2;   // 第二個實例
acc1.deposit(300);  // 第一個實例存 300
acc2.deposit(200);  // 第二個實例存 200

接著神奇的事情發生了,acc1acc2 兩個物件居然能夠直接相加,相加後的結果賦值於名為 mergedAcc 的新實例:

BankAccount mergedAcc = acc1 + acc2;

透過新的 mergedAcc 物件 getBalance() 方法就能得到相加後的餘額:

std::cout << "Merged Account Balance: $" << mergedAcc.getBalance() << "\n";

如果沒有運算子重載而想要達到一樣效果,那就必須寫成:

BankAccount mergedAcc = combineAccounts(acc1, acc2);

這個 combineAccounts 函數也要另外定義:

BankAccount combineAccounts(const BankAccount& a, const BankAccount& b) {
    BankAccount result;
    result.setBalance(a.getBalance() + b.getBalance());
    return result;
}

函數覆寫 Function Overriding

函數覆寫的多型是動態的,表示要等程式已經跑起來、真正執行到那一行時,才決定當下怎麼做。

假設要印出某位客戶所有類別帳戶的餘額,可是每種帳戶顯示餘額的方式不一樣,就可以在原本的上層類別先宣告帶有 virtual 的函數 printReport()

virtual void printReport() {
    std::cout << "[Standard Account] Current Balance: $" << balance << "\n";
}

在新類別中,再各自覆寫 printReport(),如定存帳戶中:

class FixedDepositAccount : public BankAccount {
private:
    double interestRate = 0.05;  // 定存專戶才有的利息

public:
    // 覆寫上層類別的 printReport() 方法
    void printReport() override {
        std::cout << "[Fixed Deposit Account] Balance: $" << balance 
                  << " (Estimated Annual Interest: $" << balance * interestRate << ")\n";
    }
};

還有證券帳戶的:

class SecuritiesAccount : public BankAccount {
private:
    int stockCount = 10;  // 證券帳戶專有,假設持有某股票 10 股

public:
    // 覆寫上層類別的 printReport() 方法
    void printReport() override {
        std::cout << "[Securities Account] Balance: $" << balance 
                  << " (Active Investments: " << stockCount << " shares)\n";
    }
};

main() 函數中,先定義好不同的實例:

BankAccount regular;        // 一般帳戶實例
FixedDepositAccount fxAcc;  // 定存帳戶實例
SecuritiesAccount secAcc;   // 證券帳戶實例

先給這些帳戶各自存一些金額:

regular.deposit(1000);     // 一般帳戶存 1000
fxAcc.deposit(5000);       // 定存帳戶存 5000
secAcc.deposit(3000);      // 證券帳戶存 3000

再來建立一個名為 bankSystem 的陣列,陣列的每個元素都是指向 BankAccount 物件的指標(也就是 BankAccount 物件的記憶體位址):

std::vector<BankAccount*> bankSystem;

再來依序把三個記憶體位址的指標都放到 bankSystem 陣列中:

bankSystem.push_back(&regular);
bankSystem.push_back(&fxAcc);
bankSystem.push_back(&secAcc);

雖然實際放進去的實例來自 BankAccountFixedDepositAccountSecuritiesAccount 三種不同類別,但因為 FixedDepositAccountSecuritiesAccount 都是 BankAccount 類別的小孩,所以它們的指標都會經過「向上轉型(upcasting)」,被當成上層類別指標 BankAccount*,因此也可以放進 bankSystem 陣列中。

接著進到本段落最重要的核心:

for (BankAccount* acc : bankSystem) {
    acc->printReport(); 
}

這段程式碼表示在 bankSystem 這個陣列中,對於每一個名為 acc 的「指向 BankAccount 的指標」而言,都去做當中的 printReport() 方法。

因此:

  • regular 實例執行的是寫在 BankAccount 類別的printReport()
  • fxAcc 實例執行的是寫在 FixedDepositAccount 類別的 printReport()
  • secAcc 實例執行的是寫在 SecuritiesAccount 類別的 printReport()

在程式編譯完成後的執行過程中,電腦會自己去辨認要取哪一個類別中定義的方法。

三種多型統整與綜合範例

  • 函數多載(靜態):函數名稱相同、參數不同,編譯時決定用哪個函數。
  • 運算子重載(靜態):將 +- 等運算子重定義,讓物件也可套用。
  • 函數覆寫(動態):上層類別函數寫 virtual、下層類別用 override 覆寫;下層類別指標可「向上轉型」,使上下層類別指標放在同一陣列。

三者綜合範例如下:

#include <iostream>
#include <string>
#include <vector>

class BankAccount {
protected:
    int balance = 0;

public:
    // ========= 函數多載 =========
    // 版本 A:存入現金(整數)
    void deposit(int amount) {
        if (amount > 0) {
            balance += amount;
            std::cout << "[Deposit A] Cash Deposit of $" << amount << " Made.\n";
        }
    }

    // 版本 B:存入支票(字串)
    void deposit(std::string checkNumber) {
        balance += 10000;  // 假設如果存支票就存 $10000
        std::cout << "[Deposit B] Check #" << checkNumber << " Processed. $10000 Deposited.\n";
    }

    // ========= 運算子重載 =========
    BankAccount operator+(const BankAccount& other) {
        BankAccount combinedAccount;
        combinedAccount.balance = this->balance + other.balance;
        return combinedAccount;
    }

    // ========= 函數覆寫(上層類別) =========
    virtual void printReport() {
        std::cout << "[Standard Account] Current Balance: $" << balance << "\n";
    }

    int getBalance() { return balance; }
    
    // 因為此類別內有帶 virtual 的函數,解構子也必須要有 virtual
    // 才能確保未來若透過上層類別指標銷毀物件時,下層類別專有的記憶體內容也會完整被銷毀
    // 避免記憶體洩漏
    virtual ~BankAccount() {}
};

// ========= 下層類別 1:定存專戶 =========
class FixedDepositAccount : public BankAccount {
private:
    double interestRate = 0.05;  // 假設利率為 0.05

public:
    // 函數覆寫:覆寫上層類別的 printReport() 方法
    void printReport() override {
        std::cout << "[Fixed Deposit Account] Balance: $" << balance 
                  << " (Estimated Annual Interest: $" << balance * interestRate << ")\n";
    }
};

// ========= 下層類別 2:證券專戶 =========
class SecuritiesAccount : public BankAccount {
private:
    int stockCount = 10; // 假設持有某股票 10 股

public:
    // 函數覆寫:覆寫上層類別的 printReport() 方法
    void printReport() override {
        std::cout << "[Securities Account] Balance: $" << balance 
                  << " (Active Investments: " << stockCount << " shares)\n";
    }
};

// ========= 主程式測試 =========
int main() {
    BankAccount myAccount;
    
    std::cout << "=== Testing Function Overload ===\n";
    myAccount.deposit(500);         // 函數多載版本 A (現金)
    myAccount.deposit("CH109238");  // 函數多載版本 B (支票)
    myAccount.printReport();        // 目前餘額為 500 + 10000 = 10500


    std::cout << "\n=== Testing Operator Overload ===\n";
    // 用 BankAccount 類別建立兩實例
    BankAccount acc1;
    BankAccount acc2;
    
    // 兩實例再各自取其 deposit() 函數存款
    acc1.deposit(300);
    acc2.deposit(200);

    // 運算子重載:直接用 + 號把兩個物件實例相加
    BankAccount mergedAcc = acc1 + acc2; 
    std::cout << "Merged Account Balance: $" << mergedAcc.getBalance() << "\n"; // 輸出 500


    std::cout << "\n=== Testing Dynamic Polymorphism & Function Override ===\n";
    // 建立三種不同的帳戶實例
    BankAccount regular;        // 以上層類別 BankAccount 為模板
    FixedDepositAccount fxAcc;  // 以下層類別 FixedDepositAccount 為模板
    SecuritiesAccount secAcc;   // 以下層類別 SecuritiesAccount 為模板

    // 為三個帳戶實例用 deposit() 函數存入初始金額
    regular.deposit(1000);
    fxAcc.deposit(5000);
    secAcc.deposit(3000);

    // 用上層類別的指標,把三種帳戶物件的位址統統裝進 bankSystem 陣列
    std::vector<BankAccount*> bankSystem;
    bankSystem.push_back(&regular); // 上層類別指標
    bankSystem.push_back(&fxAcc);   // 下層類別物件位址透過「向上轉型」放入陣列
    bankSystem.push_back(&secAcc);  // 下層類別物件位址透過「向上轉型」放入陣列

    std::cout << "\n--- Generating Monthly Reports via Pointer Vector ---\n";
    // 執行時期動態判斷,各自執行對應的覆寫版本
    for (BankAccount* acc : bankSystem) {
        acc->printReport(); 
    }

    return 0;
}

執行結果如下:

=== Testing Function Overload ===
[Deposit A] Cash Deposit of $500 Made.
[Deposit B] Check #CH109238 Processed. $10000 Deposited.
[Standard Account] Current Balance: $10500

=== Testing Operator Overload ===
[Deposit A] Cash Deposit of $300 Made.
[Deposit A] Cash Deposit of $200 Made.
Merged Account Balance: $500

=== Testing Dynamic Polymorphism & Function Override ===
[Deposit A] Cash Deposit of $1000 Made.
[Deposit A] Cash Deposit of $5000 Made.
[Deposit A] Cash Deposit of $3000 Made.

--- Generating Monthly Reports via Pointer Vector ---
[Standard Account] Current Balance: $1000
[Fixed Deposit Account] Balance: $5000 (Estimated Annual Interest: $250)
[Securities Account] Balance: $3000 (Active Investments: 10 shares)

抽象

指的是「只在乎要做什麼,但不在乎怎麼做」,例如 main() 函數中:

for (BankAccount* acc : bankSystem) {
    acc->printReport();  // 只在意要做 printReport(),不在意怎麼做
}

這段迴圈所在的 main() 函數只在意要做 printReport() 這件事,但一般銀行帳戶、定存帳戶跟證券帳戶分別是怎麼實現的,就留給各自的類別去處理。

純虛擬函數(介面)

目前上層類別 BankAccount 仍然有寫出執行細節:

virtual void printReport() {
    std::cout << "[Standard Account] Current Balance: $" << balance << "\n";
}

這樣其實還不夠純粹,這函數可以進一步改成在後面加上 = 0 變成「純虛擬函數(pure virtual function)」,表示這個上層類別只是一個純介面,所有繼承此類別的下層類別都要去實作自己專屬的方法:

class BankAccount {
public:
    ...

    virtual void printReport() = 0;  // 上層類別只寫 = 0 成為介面,由下層類別寫實作細節

    virtual ~BankAccount() {}
};

實作細節留給下層類別處理,例如:

class FixedDepositAccount : public BankAccount {
public:
    void printReport() override {
        std::cout << "[Fixed Deposit Account] Balance: $" << balance 
                  << " (Estimated Annual Interest: $" << balance * interestRate << ")\n";
    }
};

這樣在 main() 函數中,上層類別 BankAccount 不能被建立成一個實例,只有下層類別可以,且下層類別依然能做向上轉型:

int main() {
    // BankAccount acc;  // 錯誤:抽象類別不能做成實例

    // 下層類別才可以建立實例,且繼承了上層類別的方法
    FixedDepositAccount fxAcc;
    fxAcc.deposit(5000);

    // 下層類別依然可以向上轉型,做多型操作
    // 所以可以指向 BankAccount 的指標 acc 也能指向定存帳戶實例的位址
    BankAccount* acc = &fxAcc;
    
    // 在 BankAccount 中的 printReport() 發現帶有 virtual
    // 所以會去下層定存帳戶類別找帶有 override 的 printReport() 的實作方式
    acc->printReport();
}

範本

範本使定義的泛型(generic)函數與類別可不必擔心資料型別。

函數範本

如過要定義一個比較輸入大小、回傳較大值的函數,可以怎麼寫?

int getMax(int a, int b) {
    return (a > b) ? a : b;
}

但這裡的參數都是 int 型別,如果要換成 float 甚至是 string 呢?

// 處理 int 輸入
int getMax(int a, int b) { return (a > b) ? a : b; }
// 處理 double 輸入
double getMax(double a, double b) { return (a > b) ? a : b; }
// 處理 string 輸入
std::string getMax(std::string a, std::string b) { return (a > b) ? a : b; }

明明邏輯都一樣,卻要因為處理型別不同而寫三個函數,那何不寫一個可以通吃所有資料型別的函數?

使用範本 template 就可以實現,範例如下:

#include <iostream>
#include <string>

// 定義一個範本,裡面的 T 代表資料型別尚未決定
template <typename T>
T getMax(T a, T b) {  // 不指定型別,由傳入的參數自動推導
    return (a > b) ? a : b;
}

int main() {
    // 傳入整數,T 自動變成 int
    std::cout << getMax(10, 20) << "\n";

    // 傳入浮點數,T 自動變成 double
    std::cout << getMax(3.14, 2.71) << "\n";

    // 傳入字串,T 自動變成 std::string
    std::cout << getMax(std::string("apple"), std::string("banana")) << "\n";
}

函數只要寫一次,編譯器會在編譯時會自動判斷型別,執行結果如下:

20
3.14
banana

類別範本

除了函數,類別也可以先不指定型別,先用 T 暫時頂著,等使用者實際要使用該類別時,再指定資料型別,範例如下:

#include <iostream>

// 建立類別泛型,不指定資料型別
template <typename T>
class Box {
private:
    T item; // item 變數不寫死資料型別,以 T 暫代

public:
    Box(T inputItem) : item(inputItem) {}

    T getItem() {
        return item;
    }
};

int main() {
    // <int> 表示使用者這次指定要用 int 型別,輸入參數為 100
    Box<int> numberBox(100);
    std::cout << numberBox.getItem() << "\n";

    // <std::string> 表示使用者這次指定要用 <std::string> 型別,輸入參數為 "Arthur"
    Box<std::string> nameBox("Arthur");
    std::cout << nameBox.getItem() << "\n";
}

執行結果如下:

100
Arthur

鋪陳了這麼多 OOP 核心觀念,下一篇就可以用這些內容為基礎,來看 C++ 特殊的「智慧指標」。

參考資料

  1. Encapsulation in C++ (GeeksforGeeks)
  2. C++ Access Specifiers (W3Schools)
  3. Difference Between Constructor and Destructor in C++ (GeeksforGeeks)
  4. Inheritance in C++ (GeeksforGeeks)
  5. Diamond Problem in C++ (GeeksforGeeks)
  6. Polymorphism in C++ (GeeksforGeeks)
  7. Abstraction in C++ (GeeksforGeeks)
  8. Templates in C++ (GeeksforGeeks)

上一篇
Day 19 - volatile 的延伸問題
下一篇
Day 21 - 智慧型指標
系列文
韌體工程師的不只 0x10 個問題21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言