講義六

類別(class)

  • 類別就是使用者『自定義』的資料型態,可將類別視為一個『命名空間』,並包含一些有用的內容(如:成員函數(member function)、成員變數(member variable))。

  • 類別(Class):定義了一組屬性(成員變數)和方法(成員函數)的『藍圖』。

  • 物件(Object):根據類別創建的實例(實體)。

  • 繼承(Inheritance):可用來避免程式碼重複撰寫與降低維護困難度的機制,意指一個類別繼承另一個類別的特性(含屬性與方法)。

  • 封裝(Encapsulation):將資料和操作資料的程式碼包裝在一起。

  • 多態(又稱多態,Polymorphism):

    • 允許使用共同的介面來表現不同行為的基礎模式。

    • 多態性使得相同的介面可以用於多種不同的具體實現,從而提高了程式碼的靈活性和擴展性。

  • 『類別』,只有在 C++ 才有,C 語言中沒有『類別』的概念。

  • 實例化(Instantiation):

    • 類別本身只是一個『藍圖』,當創建類別的實例(即物件)時,才會在記憶體中分配實際的空間。

    • 依據『藍圖』,每個『實例』都擁有類別定義的屬性和方法。

  • 類別的基本思想:

    • 資料抽象(data abstraction):介面與實作的隔離。

      • 介面(interface):是指外部使用者所能執行的操作。

      • 實作(implementation):含成員變數、負責介面實作的函數(public)以及定義類別所需的私有(private)函數。

    • 封裝(encapsulation):隱藏類別的實作細節,也就是類別使用者(只能使用介面提供的功能)無法使用的部分。

  • C++ 內的每個變數都有型態(type)。

  • 在 C++ 中,可定義一個屬於某一個類別的變數(可稱為:實例化),稱其為『物件(object)』。

  • 類別(『父類別』,又稱『基礎類別』)可以衍生出『子類別』(又稱『衍生類別』)。

  • 類別本身就是一個『作用域(scope)』。

  • 以 物件名.成員名 來存取成員。

  • 若以該物件的『指標』進行存取,則使用 指標名 -> 成員名。

    struct student          // struct內的成員皆為public。C++ 也有struct。
    {
      int number;
      char name[100];
      void func(){};
    };
    
    student student1;
    student1.number = 1001;
    std::strcpy(student1.name, "Zack");
    student1.func();
    
    student *pstudent1 = &student1;
    pstudent1->number = 1005;
    
    std::cout << student1.name << std::endl;
    std::cout << student1.number << std::endl;
  • 類別中 public 修飾符所修飾的成員提供類別與外界存取的『介面(interface)』,而 private 修飾符提供各種實現類別的功能,但不曝露給外部使用者存取。

  • public 與 private 存取修飾符

    • public:公開。用此修飾符修飾的成員可以供外部使用。可視為結構或類別的『介面』。

    • private:私有。用此修飾符修飾的成員,只能被該結構或類別內部定義的成員函數使用。

  • struct(結構)成員預設為 public。

    • 語法:struct 結構名 { ... };
  • class(類別)成員預設為 private。

    • 語法:class 類別名 { ... };(有分號 ;)

    • 等價於:struct 結構名 {private: ... };(有分號 ;)

  • 『定義』在『類別內部』的函數爲『隱性』的 inline 函數。

  • 一般來說,類別的『定義』寫在標頭檔(.h),而標頭檔的檔名一般與類別名相同。

  • 類別的實作程式碼則放在一個或多個原始檔(.cpp)中,此原始檔檔名一般也與類別名相同。

  • 若有任何其他原始檔(.cpp)欲使用某個類別時,需在其原始檔的開頭 #include 該類別定義相關的『標頭檔(.h)』。

  • 建議不要將 class 與 struct 混用,否則程式碼會顯得較為混亂。

    • 按慣例可將『無』成員函數只有成員變數的資料結構定義為 struct。

    • 而把『有』成員函數的資料結構定義為 class 。

成員函數、物件複製與私有成員

成員函數

  • 成員函數範例:
// 定義Time類別
class Time
{
  public:
    int Hour;
    int Minute;
    int Second;
    void initTime(int tmpHour, int tmpmin, int tmpsec)   // 成員函數
    {
      Hour = tmpHour;
      Minute = tmpmin;
      Second = tmpsec;
    }
};

int main(int argc, char const *argv[])
{
  Time myTime;
  myTime.initTime(11, 14, 5);                           // 使用成員函數
  std::cout << myTime.Hour << std::endl;                // 11
  std::cout << myTime.Minute << std::endl;              // 14
  std::cout << myTime.Second << std::endl;              // 5
 
  return 0;
}
  • 將類別『定義』與『實作』分別放在標頭檔(MyTime.h)與原始檔(MyTime.cpp)內:

    // MyTime.h
    
    #ifndef __MyTime___
    #define __MyTime___
    
    
    class Time
    {
      public:
            int Hour;
            int Minute;
            int Second;
            void initTime(int tmpHour, int tmpmin, int tmpsec); // 成員函數的宣告。
    };
    
    
    
    #endif
    // MyTime.cpp
    
    #include "MyTime.h"
    
    void Time::initTime(int tmpHour, int tmpmin, int tmpsec) // 『Time::』 表示該函數屬於Time類別
    {
        Hour = tmpHour;
        Minute = tmpmin;
        Second = tmpsec;
    }
  • 注意:類別可以定義多次(但其他如函數,或變數只能定義一次,故類別的定義比較特殊),故類別定義可以放在標頭檔,且多個.cpp檔案都可以包含該標頭檔。

物件複製

  • 下列寫法都是『物件複製』。
Time myTime;
myTime.initTime(11, 14, 5);
// 物件複製:
Time myTime2 = myTime;
Time myTime3(myTime);
Time myTime4{myTime};
Time myTime5 = {myTime};

Time myTime6;
myTime6 = myTime5;
  • 物件複製後,每個物件都有『不同』的記憶體位址(互相獨立,互不影響),且每個物件的成員變數值都相同。

  • 物件複製:指定義一個新物件時,是用另外一個物件內的『內容』來進行『初始化』。也是就每個成員變數逐個複製。

  • 語法:透過『賦值運算子 = 』:

    • = :

    • ()

    • {}

    • ={}

  • 可透過為類別來定義『賦值運算子 = 』來控制物件的複製行為。

  • 「變數與物件的四種初始化語法」:

    寫法 範例 正式名稱
    = int a = 10; 複製初始化(copy-initialization)
    () int b(10); 直接初始化(direct-initialization)
    {} int c{10};

    直接串列初始化(direct-list-initialization)

    禁止縮窄轉換

    = {} int d = {10};

    複製串列初始化(copy-list-initialization)

    禁止縮窄轉換

    int a = 9.8;      // 可以:轉成 int,結果為 9
    int b(9.8);       // 可以:轉成 int,結果為 9
    
    int c{9.8};       // 編譯錯誤:縮窄轉換
    int d = {9.8};    // 編譯錯誤:縮窄轉換
    Time t1 = Time(9, 30, 15);  // 複製初始化
    Time t2(9, 30, 15);         // 直接初始化
    Time t3{9, 30, 15};         // 直接串列初始化
    Time t4 = {9, 30, 15};      // 複製串列初始化

私有成員

  • 類別的『私有(private)成員變數』與『私有(private)成員函數』只能被『該類別』內的成員函數所使用。

  • 設定私有成員的目的,主要是希望該介面不要曝露給外部使用者,只為『類別內』的其他成員函數使用。

建構函數、explicit 與初始化列表

  • 成員函數的『定義』:

    • 在類別定義的內部,將該成員函數完整地寫出來(實作),其中包含該成員函數的實作程式碼。
  • 成員函數的『宣告』:

    • 在類別定義的內部,只寫出該成員函數的『宣告』(.h),而具體的實作程式碼寫在類別定義的『外部』(如,寫在.cpp 檔案)。

建構函數

  • 上述 Time 類別定義一個物件時必須手動呼叫 initTime 成員函數來初始化成員變數之值。若忘記手動調用該函數,則會導致該物件內的成員變數『未被初始化』。

  • 建構函數為類別定義中的『特殊的成員函數』,其『函數名』與『類別名』相同。

  • 當創建類別之物件時,該特殊函數(建構函數)會被系統『自動』調用。該特殊成員函數則稱為『建構函數(Constructor)』。

  • 建構函數的功能:『初始化類別物件的成員變數』。

    • 建構函數『無』回傳值:不需要也不能使用 void。

    • 無法手動呼叫建構函數,否則編譯會出錯。

    • 常見的情況下,建構函數應該被修飾為 public,因創建物件時系統需要呼叫該函數。

    • 若建構函數中具有形參,則在創建物件時也應傳入相對應的『實參』。

    • 建構函數亦支援『函數重載』。

    // MyTime.h
    
    #ifndef __MyTime___
    #define __MyTime___
    
    class Time
    {
    public:
        int Hour;
        int Minute;
        int Second;
        Time(int tmpHour, int tmpmin, int tmpsec);  // 建構函數。
    private:
        int Millisecond;
        void initMilliTime(int mls);
    };
    
    #endif /* __MyTime___ */  
    // MyTime.cpp
    
    #include "MyTime.h"
    
    Time::Time(int tmpHour, int tmpmin, int tmpsec) // 成員函數,也是建構函數。
    {
        Hour = tmpHour;
        Minute = tmpmin;
        Second = tmpsec;
        initMilliTime(0);
    }
    
    void Time::initMilliTime(int mls)
    {
        Millisecond = mls;
    }
    // test.cpp
    
    #include <iostream>
    #include "MyTime.h"
    
    int main(int argc, char const *argv[])
    {
      Time myTime = Time(11, 14, 5);              // 使用建構函數
      Time myTime2(11, 14, 5);                    // 使用建構函數
      Time myTime3 = Time{11, 14, 5};             // 使用建構函數
      Time myTime4{11, 14, 5};                    // 使用建構函數
      Time myTime5 = {11, 14, 5};                 // 使用建構函數
    
      // Time myTime6();                            // 不能這樣寫。編譯器可能會誤認是函數宣告。
      // Time myTime7(11, 14);                      // 錯誤:缺少參數。
      return 0;
    }
  • 注意:下列寫法是進行『物件的複製』,並『非』呼叫『建構函數』,而是呼叫『複製(拷貝)建構函數』。

    // 執行時並非呼叫『建構函數』。
    Time myTime2_1 = myTime;
    Time myTime3_1(myTime);
    Time myTime4_1{myTime};
    Time myTime5_1 = {myTime};
  • 『複製建構函數(Copy Constructor)』

    • 定義:

      • 『複製建構函數』也是一種建構函數。

      • 當物件以『值傳遞』方式從另一個物件複製時被呼叫。

      • 會創建一個物件的副本。

    • 特點:

      • 通常接受一個對『現有物件』的『參考(Reference)』作為參數。

      • 用於實現『深複製(Deep Copy)』,特別是當物件包含『指標』或動態分配記憶體時。

      • 若未顯式定義,編譯器會提供一個『預設複製建構函數(進行淺複製)』。

    • 基本語法涉及以下要素:

      • 類別名稱:『複製建構函數』的『名稱』必須與『類別名』相同。

      • 單一參數:通常是對同一類別物件的『參考』(常定義為 const 參考)。

      • 初始化列表(可選):用於初始化成員變數。

      • 函數體:用於執行『深複製』或其他必要的初始化工作。

      class MyClass {
      public:
          MyClass(const MyClass& other) {
              // 在這裡進行深複製或其他初始化
          }
      
          // 其他成員函數和數據成員
      };
      • 上述範例中,MyClass 的『複製建構函數』接受一個對 MyClass 類別物件的『參考』(const MyClass& other),並使用這個『參考』來初始化新物件的狀態。
    • 注意:

      • 建構函數:

        • 確保當類別的物件被創建時,會以適當的初始狀態開始。
      • 複製建構函數:

        • 用於管理物件如何被複製,尤其是當物件涉及到資源管理(例如動態配置記憶體)時。

        • 不恰當的複製可能導致資源洩漏或重複釋放等問題。

    • 補充:

      • 淺複製(Shallow Copy):

        • 定義:

          • 淺複製是將物件中的成員進行逐成員複製(memberwise copy)。

          • 對一般資料成員,例如 int、double,會直接複製其值。

          • 如果成員是指標(pointer),則複製的是指標本身所儲存的記憶體位址,而不是另外複製該指標所指向的資料。

        • 可能產生的問題:

          • 共享同一份動態資源:

            • 複製後,原物件與新物件中的指標可能指向同一塊動態配置的記憶體。

            • 因此,透過其中一個物件修改該資料時,另一個物件也會觀察到相同的變化。

          • 懸空指標(dangling pointer):

            • 如果其中一個物件先釋放共享的記憶體,另一個物件中的指標仍保存原本的位址,但該記憶體已經失效。

            • 此時再次存取該指標會造成未定義行為(undefined behavior)。

          • 重複釋放(double delete):

            • 如果兩個物件都認為自己擁有同一塊動態記憶體,並在解構時分別執行 delete,就可能對同一塊記憶體進行兩次釋放。

            • 這同樣屬於未定義行為,可能造成程式當機或記憶體錯誤。

        • 使用情境:

          • 如果類別只包含一般值型別成員,或成員本身能正確管理自己的資源,例如 std::string、std::vector,通常可以直接使用編譯器提供的預設複製行為。

          • 真正需要特別注意的是:類別若使用 raw pointer 管理並**擁有(own)**動態資源,單純的 memberwise copy 通常不足以正確處理資源所有權。

        • 物件本身雖然是兩個不同的物件,但其中的指標保存相同的位址,因此兩者共享同一份動態資源。

        • 回憶:Raw pointer(裸指標、原始指標)是 C++ 內建的指標型別,本身只儲存記憶體位址,不會自動管理所指向資源的生命週期。當 raw pointer 搭配 new / delete 管理動態記憶體時,程式設計者必須自行負責資源的配置與釋放。

      #include <iostream>
      using namespace std;
      
      class MyClass {
      public:
        int* data;
      
        MyClass(int value) {
          data = new int(value);
        }
      };
      
      int main() {
        MyClass obj1(10);
      
        // 使用預設複製建構函數
        // 進行 memberwise copy
        MyClass obj2 = obj1;
      
        cout << "obj1.data address: " << obj1.data << endl;
        cout << "obj2.data address: " << obj2.data << endl;
      
        cout << "obj1 value: " << *obj1.data << endl;
        cout << "obj2 value: " << *obj2.data << endl;
      
        // 透過 obj2 修改資料
        *obj2.data = 100;
      
        cout << "\nAfter modifying obj2:" << endl;
      
        cout << "obj1 value: " << *obj1.data << endl;
        cout << "obj2 value: " << *obj2.data << endl;
      
        // 為避免記憶體洩漏,只釋放一次
        delete obj1.data;
      
        return 0;
      }
      • 深複製(Deep Copy):

        • 定義:

          • 深複製是對物件所管理的資料進行獨立複製。

          • 如果物件內含指標並擁有該指標所指向的動態資源,深複製會為新物件配置新的記憶體空間,再將原物件中的資料複製到新的記憶體中。

          • 因此,複製完成後,原物件與新物件各自擁有獨立的資源,兩者不會共用同一塊動態記憶體。

          • 當類別自行管理動態資源時,通常需要自行定義適當的**複製建構函數(copy constructor)**與複製行為。

        • 特性:

          • 資源彼此獨立:

            • 原物件與新物件中的指標會指向不同的記憶體位置。

            • 雖然兩塊記憶體中的資料內容在複製當下相同,但之後修改其中一個物件的資料,不會影響另一個物件。

          • 獨立管理記憶體:

            • 新物件會在堆(heap)配置自己的動態記憶體,並將原物件所管理的資料複製過去。

            • 因此,每個物件都可以獨立管理並釋放自己的資源。

          • 避免資源共享造成的問題:

            • 因為兩個物件不再共用同一塊動態記憶體,可以避免因資源所有權不清而產生的**懸空指標(dangling pointer)與重複釋放(double delete)**問題。
        • 使用情境:

          • 當類別使用 raw pointer 管理且擁有(own)動態配置的資源,而複製後的物件需要各自擁有獨立資源時,就需要實作適當的深複製。

          • 若使用 std::string、std::vector 等能自行管理資源的標準容器,通常不需要自行撰寫深複製邏輯。

        #include <iostream>
        
        class MyClass {
        public:
            int* data;
        
            // 構造函數
            MyClass(int value) { 
                data = new int(value); 
                std::cout << "Constructed with value: " << value << std::endl;
            }
        
            // 『複製建構函數 (深複製)』
            MyClass(const MyClass& other) {
                data = new int(*other.data);  // 深複製
                std::cout << "Copy constructed with value: " << *other.data << std::endl;
            }
        
            // 解構函數
            ~MyClass() { 
                delete data; 
                std::cout << "Destructed" << std::endl;
            }
        
            // 『(複製)賦值運算子 (深複製)』
            MyClass& operator=(const MyClass& other) {
                if (this == &other) {
                    return *this; // 防止自我賦值
                }
        
                delete data; // 釋放舊的資源
                data = new int(*other.data); // 深複製
        
                std::cout << "Copy assigned with value: " << *other.data << std::endl;
                return *this;
            }
        
            // 其他成員函數
            void print() const {
                std::cout << "Value: " << *data << std::endl;
            }
        };
        
        int main() {
            MyClass obj1(42);
            MyClass obj2 = obj1; // 使用複製建構函數
        
            obj1.print(); // 輸出 42
            obj2.print(); // 輸出 42
        
            MyClass obj3(84);
            obj3 = obj1; // 使用『複製賦值運算子』
        
            obj3.print(); // 輸出 42
        
            return 0;
        }
    • 在這個例子中,『複製建構函數』為 data 指標變數分配了新的記憶體,並複製了原始物件中的值,從而實現了『深複製』。因此,即使原始物件被銷毀或更改,複製的物件也會保持自己的狀態。

    • 其中, data = new int(*other.data);

      • other.data:這部分程式碼存取傳入物件 other 的 data 成員。由於 other 是一個對 MyClass 物件的『參考』,因此 other.data 指向 other 物件中的動態分配整數。

      • *other.data:在這裡,* 是解參考運算子,取得 other.data 指標所指向的實際整數值。

      • new int(...):new 是一個動態記憶體分配運算符。它在『堆(stack)』上為一個整數分配記憶體,並回傳這塊新分配記憶體的位址。在括號內,將 *other.data 的值傳遞給 new int,這樣就在『堆(heap)』上創建一個新的整數,其值與 other.data 所指向的整數相同。

      • data = ...:最後,將 new int(...) 回傳的新記憶體地址賦值給當前物件的 data 指針。這樣,當前物件的 data 指標就指向了一塊新的、僅屬於此物件的記憶體空間,這塊記憶體包含了與 other 物件的 data 成員相同的值。

多個建構函數

  • 類別中可存在多個『建構函數』,其意指可為該類別物件提供多種創建物件的方法

  • 建構函數重載:參數的「數量、型態或型態排列順序」有所不同即可,只改變參數名稱不能形成重載。

  • 多個『建構函數』需具有『參數數量』與『參數型態』的差異。

  • 預設參數的位置:分開撰寫 .h 與 .cpp 時,通常將預設值放在 .h 的宣告中,讓呼叫端看得到;.cpp 的定義不要重複指定。這是宣告可見性的問題,不是副檔名本身的規定。

  • EX:

    // MyTime.h
    
    #ifndef __MyTime___
    #define __MyTime___
    
    class Time
    {
    public:
        int Hour;
        int Minute;
        int Second;
        Time();                      // 宣告一個『無參數』的建構函數
        Time(int tmpHour, int tmpmin, int tmpsec); 
    private:
        int Millisecond;
        void initMilliTime(int mls);
    };
    
    #endif /* __MyTime___ */
    // MyTime.cpp
    
    #include "MyTime.h"
    
    Time::Time(int tmpHour, int tmpmin, int tmpsec) 
    {
        Hour = tmpHour;
        Minute = tmpmin;
        Second = tmpsec;
        initMilliTime(0);
    }
    
    Time::Time()                     // 『無參數』建構函數的實作。
    {
        Hour = 12;
        Minute = 59;
        Second = 59;
        initMilliTime(59);
    }
    
    void Time::initMilliTime(int mls)
    {
        Millisecond = mls;
    }
    // 執行無參數的建構函數。
    Time myTime10 = Time();         // 呼叫『無參數』建構函數
    Time myTime12;                  // 呼叫『無參數』建構函數。注意寫法,只有物件名。
    Time myTime13 = Time{};         // 呼叫『無參數』建構函數
    Time myTime14{};                // 呼叫『無參數』建構函數。注意寫法,跟著一個{}
    Time myTime15 = {};             // 呼叫『無參數』建構函數
  • 建構函數的『預設參數』:

    • 任何函數都可以有預設參數(當然包括『建構函數』)。

    • 對於一般函數而言,預設參數會放在『函數宣告』(.h),除非該函數的『函數定義』。

    • 類別中的成員函數,預設參數會寫在『成員函數宣告』(.h),而不是實作部分(.cpp)。

    • 函數預設參數的規定:在具有多個參數的函數中指定參數預設值時,預設參數都必須出現在『無預設參數』的『右側』。

    Time(int tmphour, int tmpmin = 59, int tmpsec = 12);    // 正確。
    // Time(int tmphour, int tmpmin = 59, int tmpsec);         // 錯誤。
    class Time
    {
    public:
      Time();
      Time(int h = 12, int m = 59, int s = 59);
    };
    
    Time t;   // 編譯錯誤:兩個建構函數都能接受零個引數,無法決定

隱性轉換與 explict

  • 隱性轉換:

    • 程式沒有明確寫出轉換操作,編譯器依照規則,自動把某個型態轉成另一個型態。

    • 當不同型態之間可以合理轉換時,C++ 編譯器可能會自動進行型態轉換。

    • 例如:當 int 與 double 做運算時,編譯器會將 int 轉換成 double 型態後再進行運算。

    • 對類別而言,未加上 explicit 的建構函數,可能成為隱性轉換的途徑

  • 以『單參數』的『建構函數』來說明『隱性轉換』:

    // Time myTime23 = 14;                         // 語法錯誤。
    // Time myTime24 = (12, 13, 14, 15, 16);       // 語法錯誤。無論()內有多少數字。
    • 接著宣告並實作『單參數』的『建構函數』:

      // MyTime.h
      
      #ifndef __MyTime___
      #define __MyTime___
      
      class Time
      {
      public:
          int Hour;
          int Minute;
          int Second;
          Time();
          Time(int tmphour);           
          Time(int tmpHour, int tmpmin, int tmpsec); 
      private:
          int Millisecond;
          void initMilliTime(int mls);
      };
      
      #endif /* __MyTime___ */
      // MyTime.cpp
      
      #include "MyTime.h"
      
      Time::Time(int tmpHour, int tmpmin, int tmpsec) // 成員函數
      {
          Hour = tmpHour;
          Minute = tmpmin;
          Second = tmpsec;
          initMilliTime(0);
      }
      
      Time::Time()
      {
          Hour = 12;
          Minute = 59;
          Second = 59;
          initMilliTime(59);
      }
      
      Time::Time(int tmphour)
      {
          Hour = tmphour;
          Minute = 59;
          Second = 59;
          initMilliTime(59);
      }
      
      void Time::initMilliTime(int mls)
      {
          Millisecond = mls;
      }
    Time myTime23 = 14;                         // 語法正確。
    Time myTime24 = (12, 13, 14, 15, 16);       // 語法正確。
    • 現在上述程式碼可以編譯成功。且都調用『單參數』構造函數。

    • 其中,Time myTime24 = (12, 13, 14, 15, 16); 只有『最後一個數字 16』 作為參數傳遞到單參數構造函數。

    • 上述程式碼皆發生了『隱性形態轉換』。

  • 加入一個新的普通函數:

    void func(Time myt)    // 形式參數:myt
    {
      return;
    }
    func(16);
    myTime23 = 16;         // 同樣調用Time類別的單參數建構函數,產生一個臨時變數,接著將此變數值複製到myTime23的成員變數。
    • 可發現用一個數字就能夠呼叫 func 函數:func(16);,可以編譯成功。

    • 這說明了編譯器進行一個從數字 16 轉換為 myt 物件(臨時物件,其型態為 Time),函數呼叫完畢後,物件 myt 的生存期結束,所佔用的資源則被系統回收。

    Time myTime100 = {16};   // 明確讓系統呼叫帶一個參數的建構函數。
    Time myTime101 = 16;     // 程式碼較模糊不清,存在『隱性轉換』的問題。
    func(16);                // 程式碼較模糊不清,存在『隱性轉換』的問題。
  • 使用 explicit 禁止隱性轉換:

    • 可在建構函數的『宣告』加上 explicit ,明確要求建構函數『不要』進行『隱性轉換』。

      class Money {
      public:
        double value;
      
        explicit Money(double v) {
          value = v;
        }
      };
      
      Money m(100.0);
      
      Money m = 100.0;  // 錯誤。但只要 Money 有一個可接受 double 的非 explicit 建構函數,這就是合法的 C++。
      /* class Money {
      public:
        double value;
      
        Money(double v) {
          value = v;
        }
      };   
      
        Money m = 100.0;    // 可以把 double 轉成 Money,所以會使用這個建構函數初始化 m。
      
       */
      • Money m = 100.0; copy initialization(複製初始化)

      • Money m(100.0); direct initialization(直接初始化)

      • 這兩者在這個例子中都會呼叫 Money(double),來建立 Money 物件。

        • 也就是說:

          Money m = 100.0;

          雖然看起來像是「把 100.0 指派給 m」,但其實此時 m 正在被建立,因此這不是指定(assignment),而是初始化(initialization)。

          編譯器會利用:

          Money(double)

          將 100.0 轉換成一個 Money 物件。

          概念上可以理解為:

          Money m = Money(100.0);
      • 如果建構函數為:

        Money(double v);

        則下面兩種寫法都可以:

        Money m1 = 100.0;   // OK:copy initialization
        Money m2(100.0);    // OK:direct initialization

        但如果改成:

        explicit Money(double v);

        則:

        Money m1 = 100.0;   // 錯誤:不允許隱性轉換
        Money m2(100.0);    // OK:明確呼叫建構函數

        因此可以簡單記成:

        copy initialization 可能需要編譯器進行隱性型態轉換;direct initialization 則是明確指定使用某個建構函數來建立物件。

        而 explicit 的作用,就是防止建構函數被用來進行這類非預期的隱性轉換。

  • 例如:在帶有三個參數的 Time 類別建構函數宣告前面加上 explicit(修改 MyTime.h):

    // MyTime.h
    
    #ifndef __MyTime___
    #define __MyTime___
    
    class Time
    {
    public:
        int Hour;
        int Minute;
        int Second;
        Time();
        Time(int tmphour);           
        explicit Time(int tmpHour, int tmpmin, int tmpsec);       // 加上explicit
    private:
        int Millisecond;
        void initMilliTime(int mls);
    };
    
    #endif /* __MyTime___ */
    #include <iostream>
    #include "utilities.h"
    #include "MyTime.h"
    
    int main(int argc, char const *argv[])
    {
      // Time myTime5 = {12, 13, 52};       // 錯誤
      Time myTime6{12, 13, 52};             // 正確
      return 0;
    }
    • 上述例子可發現,加上 = 號,就會變成『隱性轉換』;省略等號 =,則為『顯性初始化』(直接初始化)。
  • 一般來說,除非有特殊原因,『單參數』建構函數都會宣告為 explicit。

    explicit Time();              // explicit用於無參數建構函數。
    
    Time time1{};                 // 正確:顯性轉換
    // Time time2 = {};              // 錯誤:隱性轉換,多了一個等號。
    // func({});                     // 錯誤:隱性轉換。
    // func({1, 2, 3});              // 錯誤:隱性轉換。
    func(Time{});                    // 正確:顯性轉換,產生臨時物件,呼叫無參數建構函數。
    func(Time{1, 2, 3});             // 正確:顯性轉換,產生臨時物件,呼叫三個參數建構函數。

建構函數『成員初始化列表』

  • 在C++中,建構函數『成員初始化列表』是用於在建構函數體執行前初始化類的成員變數的一種方法。它為『非基本類型』(類別型態)的成員變數提供了一種更高效的初始化方式。

  • 呼叫建構函數時,建構函數的『成員初始化列表』可初始化成員變數。

  • 『冒號括號逗點(:(),)寫法』:寫在建構函數的實作(定義)中。

  • 建構函數的成員初始化列表(member initializer list),是用來在進入建構函數函數體之前,直接初始化資料成員的一種語法。

    Time::Time(int tmphour, int tmpmin, int tmpsec)
      :Hour(tmphour), Minute(tmpmin)                   // 建構函數初始化列表
    Time::Time(int tmphour, int tmpmin, int tmpsec)   // 函數體進行成員變數賦值。
    {
      Hour = tmphour;
      Minute = tmpmin;
    }
    class MyClass {
        int number;
        std::string text;
    
    public:
        MyClass(int num, std::string txt) : number(num), text(txt) {
            // 建構函數函數體
        }
    };
  • 『建構函數初始化列表』與『函數體內賦值』都可以實作成員變數的初始化,但有些操作必須使用『建構函數初始化列表』才行。故建議採『建構函數初始化列表』。

  • 若成員變數為『內建基本型態』(如 int 等),『建構函數初始化列表』與『函數體內賦值』差異不大。

  • 但對於其他類別型態(非內建基本型態,如類別型態),則『建構函數初始化列表』初始化的『效率更好』。

  • 『建構函數初始化列表』成員變數建立的順序並『非』按照初始化列表從左至右的順序,而是按照類別定義中成員變數的定義順序。

  • 使用成員初始化列表的優勢包括:

    • 效率:

      • 對於『非基本類型』(類別型態)的成員,使用初始化列表比在建構函數體內賦值更高效,因為它避免了額外的構造函數和解構函數的呼叫。

      • 直接初始化通常更有效率,因為可以避免「先建構,再指定」的額外操作。

    • 初始化 const 成員與參考成員:只能在初始化列表中初始化 const 成員和參考成員。因為它們一旦被建構後就不能被賦值(注意const 成員不能在建構函数體内對它們進行賦值操作)。

      class Test {
      private:
        int& ref;
      
      public:
        Test(int& value)
          : ref(value) {
        }
      };
      
      // reference 必須在建立時就綁定對象,因此也必須使用初始化列表。
      • 補充:

        • 『參考』成员在建構函数的初始化列表中被初始化,則會一直參考最初绑定的物件,無法更改參考的標的物。

        • 『參考』成員選擇使用 const 或非 const 參考取決於是否希望允許透過該『參考』修改所引用的物件。

    • 初始化基礎類別(又稱父類別)和成員物件:若類別是從其他類別繼承而來的,或者有物件類型的成員,則這些基礎類別(父類別)和成員物件可以透過『初始化列表』進行初始化。

    #include <iostream>
    #include <string>
    
    class Engine {
    public:
        std::string type;
    
        Engine(std::string t) : type(t) {}
    };
    
    class Car {
        const int id;        // const 成員
        int& mileage;        // 參考成員
        std::string model;   // 物件成員
        Engine engine;       // 另一個物件成員
    
    public:
        Car(int carId, int& carMileage, std::string carModel, std::string engineType) 
            : id(carId), mileage(carMileage), model(carModel), engine(engineType) {
            // 建構函數體,可用於進行其他初始化工作或邏輯處理
        }
    
        void display() {
            std::cout << "Car ID: " << id << "\n";
            std::cout << "Mileage: " << mileage << "\n";
            std::cout << "Model: " << model << "\n";
            std::cout << "Engine Type: " << engine.type << "\n";
        }
    };
    
    int main() {
        int mileage = 10000;
        Car myCar(123, mileage, "Toyota", "V8");
    
        myCar.display();
        return 0;
    }
    • Car 類包含四個成員:id(const int),mileage(int 參考),model(std::string 物件),和 engine(Engine 類別的物件)。

    • id 作為 const 成員,在建構函數初始化列表中被初始化。

    • mileage 是一個『參考』成員,在建構函數初始化列表中被初始化。

    • model 和 engine 是『物件』成員,它們也在『建構函數初始化列表』中被初始化。

    • Engine 類別是一個簡單的類別,包含一個 std::string 成員 type,用於表示引擎的型號。

    • 在 main 函數中,創建了一個 Car 物件 myCar,並透過 display 方法展示其訊息。

inline 成員函數

  • 直接寫在『類別定義』內實作的成員函數會被當作 inline 函數做處理。

    • inline 函數:編譯器會嘗試將函數體內的程式碼直接取代函數呼叫的程式碼,可提高程式執行的效率。

    • inline 函數只是對編譯器的建議,能否 inline 成功,取決於編譯器。當成員函數的定義簡單,可提高函數內聯成功的機率。

    #ifndef __MyTime___
    #define __MyTime___
    
    #include <iostream>
    
    class Time
    {
    public:
        int Hour;
        int Minute;
        int Second;
        explicit Time();
        Time(int tmphour);           
        explicit Time(int tmphour, int tmpmin, int tmpsec);
        //////////////////////////////////////////////////////////////////////////
        void addHour(int tmphour)                               // inline成員函數。
        {
            Hour += tmphour;
        }
        /////////////////////////////////////////////////////////////////////////
    private:
        int Millisecond;
        void initMilliTime(int mls);
    };
    
    #endif /* __MyTime___ */

成員函數末尾加上 const

  • 作用:代表該成員函數『無法』修改該物件的任何成員變數之值。

  • 注意:若想在成員函數末尾加上 const,必須在函數『宣告』與『定義』加入 const。

  • 該函數被稱為:『const 成員函數』。

  • 『const 成員函數』可被 『const 物件』或『非 const 物件』所調用。

    可在 MyTime.h 加入一個新的成員函數:

    void noone() const                // 成員函數末尾加上 const。
    {
      Hour += 10;                     // 錯誤:const成員函數不可以修改成員變數值。
    }
    #ifndef __MyTime___
    #define __MyTime___
    
    #include <iostream>
    
    class Time
    {
    public:
        int Hour;
        int Minute;
        int Second;
        explicit Time();
        Time(int tmphour);           
        explicit Time(int tmphour, int tmpmin, int tmpsec);
        void addHour(int tmphour)
        {
            Hour += tmphour;
        }
        /////////////////////////////////////////////////////////////////////////////
        void noone() const                             // 成員函數末尾加上 『const』。
        {
            std::cout << "執行 noone函數" << std::endl;
        }
        /////////////////////////////////////////////////////////////////////////////
    private:
        int Millisecond;
        void initMilliTime(int mls);
    };
    
    #endif /* __MyTime___ */
    // tesp.cpp
    
    #include <iostream>
    #include "utilities.h"
    #include "MyTime.h"
    
    int main(int argc, char const *argv[])
    {
      const Time abc;
      // abc.addHour(12);
      abc.noone();
    
      Time def;
      def.addHour(12);
      def.noone();
    
      return 0;
    }
  • 普通函數(非成員函數)末尾不能加上 const,無法編譯成功。

mutable

  • 成員函數末尾加上 const,是不允許修改成員變數的。但可透過關鍵字 mutable 修飾某成員變數,來修改特定成員變數。

  • 也可以試著將成員函數的 const 拿掉來修改成員變數,但會導致 『const 物件』無法呼叫原來的成員函數。

  • 故可此用關鍵字 mutable 來修飾某一『成員變數』,使得該成員變數處於『可變狀態(mutable)』,即使是在以 const 結尾的成員函數中。

    #ifndef __MyTime___
    #define __MyTime___
    
    #include <iostream>
    
    class Time
    {
    public:
        mutable int Hour;                                   // 加上mutable修飾符
        int Minute;
        int Second;
        explicit Time();
        Time(int tmphour);           
        explicit Time(int tmphour, int tmpmin, int tmpsec);
        void addHour(int tmphour)
        {
            Hour += tmphour;
        }
        void noone() const
        {
            Hour += 3;                                      // 可以用來修改Hour
        }
    
    private:
        int Millisecond;
        void initMilliTime(int mls);
    };
    
    #endif /* __MyTime___ */

this 指標

  • this:是指向目前物件本身的指標。

  • 在每一個非 static 成員函數中,都可以使用 this 來取得呼叫該成員函數之物件的位址。

  • this 在成員函數是一個『隱藏』形式參數,用來表示指向自身物件的『指標變數』。

  • 在 C++ 中,成員函數背後會隱含地傳遞 this 指標給函數,讓該函數知道它正操作的是哪個物件的資料。

  • *this 表示該指標所指向的物件(對指標變數進行解參考),即為『物件本身』。也就是呼叫該成員函數的物件。

  • static 成員函數無法使用 this:

    • static 成員函數是類別層級的,與某個具體物件無關,因此沒有 this 指標。
  • 當物件呼叫成員函數時,編譯器會將呼叫此成員函數的物件的位址傳遞給該成員函數的『隱藏』形式參數,其名為『this』。

    class Test {
    public:
      int x;
    
      void show() {
        cout << this << endl;
      }
    };
    
    Test obj;
    obj.show();
    // MyTime.h
    
    #ifndef __MyTime___
    #define __MyTime___
    
    #include <iostream>
    
    class Time
    {
    public:
        mutable int Hour;
        int Minute;
        int Second;
        explicit Time();
        Time(int tmphour);           
        explicit Time(int tmphour, int tmpmin, int tmpsec);
        void addHour(int tmphour)
        {
            Hour += tmphour;
        }
        void noone() const
        {
            Hour += 3;
        }
        Time& rtnhour(int tmphour);               // 『回傳物件本身(Time&)的函數』。
    
    private:
        int Millisecond;
        void initMilliTime(int mls);
    };
    
    #endif /* __MyTime___ */
    // MyTime.cpp
    
    #include "MyTime.h"
    
    Time::Time(int tmphour, int tmpmin, int tmpsec) // 成員函數
    {
        Hour = tmphour;
        Minute = tmpmin;
        Second = tmpsec;
        initMilliTime(0);
    }
    
    Time::Time()
    {
        Hour = 12;
        Minute = 59;
        Second = 59;
        initMilliTime(59);
    }
    
    Time::Time(int tmphour)
    {
        Hour = tmphour;
        Minute = 59;
        Second = 59;
        initMilliTime(59);
    }
    
    void Time::initMilliTime(int mls)
    {
        Millisecond = mls;
    }
    
    Time& Time::rtnhour(int tmphour)
    {
        Hour += tmphour;
        return *this;                            // 回傳*this。
    }
    #include <iostream>
    #include "utilities.h"
    #include "MyTime.h"
    
    int main(int argc, char const *argv[])
    {
      Time mytime;
      mytime.rtnhour(3);
    
    
      return 0;
    }
  • 上述範例,編譯器在內部實際上重寫 rtnhour 成員函數,如下:

    Time& Time::rtnhour(Time* const this, int tmphour ) {.........};  // 注意 const出現的位置。
  • this 是『系統保留字』,故參數名、變數名等都『不能』被命名為 this。

  • this 指標變數只能在『成員函數』中使用,全局變數、『靜態(static)函數(static 成員函數沒有 this)』等都不能使用 this 指標。

  • 在『一般成員函數』中,this 為一個指向『非 const 物件』的 『const 指標變數』(Time* const this 型態 )。

    • this 只能指向該 Time 物件(const 指標變數),無法再指向其他物件了。
  • 在 『const 成員函數』中,this 是一個指向 『const 物件』的 『const 指標變數』(const Time* const this 型態 )。

  • 若加上另一個函數:

    // MyTime.h
    
    #ifndef __MyTime___
    #define __MyTime___
    
    #include <iostream>
    
    class Time
    {
    public:
    
        mutable int Hour;
    
        int Minute;
        int Second;
        explicit Time();
        Time(int tmphour);           
        explicit Time(int tmphour, int tmpmin, int tmpsec);
        void addHour(int tmphour)
        {
            Hour += tmphour;
        }
    
        void noone() const
        {
            Hour += 3;
        }
    
        Time& rtnhour(int tmphour);
        Time& rtnminute(int tmpminute);         // 新函數
    
    private:
        int Millisecond;
        void initMilliTime(int mls);
    };
    
    #endif /* __MyTime___ */
    // MyTime.cpp
    
    #include "MyTime.h"
    
    Time::Time(int tmphour, int tmpmin, int tmpsec) // 成員函數
    {
        Hour = tmphour;
        Minute = tmpmin;
        Second = tmpsec;
        initMilliTime(0);
    }
    
    Time::Time()
    {
        Hour = 12;
        Minute = 59;
        Second = 59;
        initMilliTime(59);
    }
    
    Time::Time(int tmphour)
    {
        Hour = tmphour;
        Minute = 59;
        Second = 59;
        initMilliTime(59);
    }
    
    void Time::initMilliTime(int mls)
    {
        Millisecond = mls;
    }
    
    Time& Time::rtnhour(int tmphour)
    {
        Hour += tmphour;
        return *this;
    }
    
    Time& Time::rtnminute(int tmpminute)             // 新函數
    {
        Minute += tmpminute;
        return *this;
    }
    #include <iostream>
    #include "utilities.h"
    #include "MyTime.h"
    
    int main(int argc, char const *argv[])
    {
      Time mytime;
      mytime.rtnhour(3).rtnminute(5);              // 兩個函數串起使用。
      return 0;
    }
  • 回傳當前物件 (*this)

    #include <iostream>
    using namespace std;
    
    class Example {
    private:
        int value;
    public:
        Example& setValue(int value) {
            this->value = value;
            return *this;  // 回傳當前物件的引用
        }
        void printValue() const {
            cout << "Value: " << this->value << endl;
        }
    };
    
    int main() {
        Example obj;
        obj.setValue(10).setValue(20).setValue(30);  // 鏈式調用
        obj.printValue();
        return 0;
    }
    • setValue 回傳當前物件的引用 (*this),允許函數呼叫 setValue 可以直接串接執行。
    #include <iostream>
    
    class Account {
        double balance_ = 0;
    public:
        Account& deposit(double amount) {
            balance_ += amount;
            return *this;  // 回傳當前物件,實現 method chaining
        }
    
        Account& withdraw(double amount) {
            balance_ -= amount;
            return *this;
        }
    
        void show() const {
            std::cout << "Balance: " << balance_ << std::endl;
        }
    };
    
    int main() {
        Account a;
        a.deposit(1000).withdraw(200).show();  // method chaining
    }
  • 在賦值運算子(operator=)重載中,使用 this 指標判斷是否為自我賦值,避免意外覆蓋。

    #include <iostream>
    using namespace std;
    
    class Example {
    private:
        int* data;
    public:
        Example(int value) : data(new int(value)) {}
        ~Example() { delete data; }
    
        Example& operator=(const Example& other) {
            if (this == &other) {  // 判斷是否是自我賦值
                return *this;
            }
            *data = *other.data;  // 深複製
            return *this;
        }
    
        void printValue() const {
            cout << "Value: " << *data << endl;
        }
    };
    
    int main() {
        Example obj1(42);
        obj1 = obj1;  // 自我賦值
        obj1.printValue();
        return 0;
    }
  • 傳遞當前物件的指標:this 指標可以作為參數傳遞給其他函數或方法,表示當前物件。

    #include <iostream>
    using namespace std;
    
    class Example;
    
    class Helper {
    public:
        void showValue(const Example* obj);
    };
    
    class Example {
    private:
        int value;
    public:
        Example(int value) : value(value) {}
    
        void show(Helper& helper) {
            helper.showValue(this);  // 將當前物件的指標傳遞
        }
    
        int getValue() const {
            return value;
        }
    };
    
    void Helper::showValue(const Example* obj) {
        cout << "Value: " << obj->getValue() << endl;
    }
    
    int main() {
        Example obj(42);
        Helper helper;
        obj.show(helper);
        return 0;
    }
    • 在成員函數 show 中,將當前物件的指標 (this) 傳遞給外部函數,實現跨類別的操作。

static 成員

static 成員變數

  • 一般的『成員變數』屬於『物件本身』。故兩個不同物件的成員變數互相獨立,互不影響。

  • 當『成員變數』使用關鍵字 static 修飾,此 『static 成員變數』不屬於物件,而是屬於整個類別。

  • 所有物件共用一份資料。

  • 常用於統計數量、共用設定。

  • 『static 成員變數』無法透過物件來修改,但可透過類別來修改。

    MyClass::staticVar = 42;  // 修改 static 成員變數的值
    int value = MyClass::staticVar;  // 存取 static 成員變數的值
  • 對於一般成員變數,每個物件針對該成員變數都有自己的『副本』,可以保存不同的值。

  • 對於『static 成員變數』是所有該類別的物件『共享』一個『副本』。

  • 一般會在原始檔案(.cpp)的開頭來定義『靜態成員變數』(在類別的實作.cpp檔案),確保在任何函數之前,此『static 成員變數』已經被成功初始化,以確保這個『static 成員變數』可以被正常使用。

    // MyClass.h
    class MyClass {
    public:
        static int counter;   // 宣告:告訴編譯器有這個成員
    };
    // MyClass.cpp
    // 必須在 某個 .cpp 檔案 中「定義」它(給出實體空間),否則會出現 linker 錯誤(undefined reference):
    #include "MyClass.h"
    
    // 把靜態成員的「定義與初始化」放在 .cpp 檔最上面(所有函數之前),
    // 能確保在該檔案裡任何使用它的函數之前,它一定已經初始化。
    int MyClass::counter = 0;   // 定義 + 初始化
    // MyClass.cpp
    #include "MyClass.h"
    
    // 靜態成員初始化放在最前面
    int MyClass::counter = 0;
    
    // 其他靜態成員也集中放這裡
    std::string MyClass::defaultName = "Guest";
    
    // 之後才是成員函數的實作
    MyClass::MyClass() {
        ++counter;
    }
    
    void MyClass::printInfo() {
        std::cout << "Name: " << defaultName << ", Count: " << counter << std::endl;
    }
  • 定義與初始化:為了分配儲存空間,static 成員變數需要在『類別外部』進行定義和初始化(除了 const static 整數型態成員和 constexpr 成員,這些可以在『類別內』部直接初始化)。

  • 從 C++17 起,若你使用 inline static 或 constexpr,就可以直接在類別內初始化,不需要在 .cpp 檔定義:

    • 因為使用 inline static,這一行同時完成:

      • 宣告

      • 定義

      • 初始化

      不需要額外在類別外定義。

    • C++17 起,static constexpr 隱含 inline,因此不再需要額外定義。

    • 而 static constexpr 必須使用符合常數表達式要求的初始化值,其物件不可在初始化後修改。

    • 而且可以安全地將此類別定義放在標頭檔中,供多個 .cpp 檔案使用。

    • static const 與 static constexpr 的例外

      宣告方式 C++11/14 類別內初始化 C++17 以後類別內初始化
      static int x 不可 不可,除非加上 inline
      static const int x 可以 可以
      static constexpr double x 可以 可以
      inline static int x 不支援 可以
      inline static std::string x 不支援 可以
      // C++17 起可這樣寫:
      class MyClass {
      public:
          inline static int counter = 0;
          static constexpr double pi = 3.1415926;
      };
  • 生命週期:

    • static 資料成員具有 靜態儲存期(Static Storage Duration)。

      • 其儲存空間在整個程式執行期間持續存在。

      • 不會因為某個類別物件的建立或銷毀而配置或釋放。

      • 若為需要動態初始化的類別型別,實際建構時機可能晚於程式啟動。

      • 若需要解構,通常在程式正常終止期間執行解構函數。

      class Counter {
      public:
        inline static int count = 0; // C++17
      };
      
      int main() {
        Counter a;
        Counter b;
      
        Counter::count = 10;

      即使 a 與 b 都被銷毀,Counter::count 的儲存空間仍會持續存在,直到程式終止。

  • 儲存類型:其儲存在程式的數據區域,而『非』堆(heap)或棧(stack)。

    #include <iostream>
    
    class MyClass {
    public:
        static const int constStaticValue = 10;    // 直接在類別內初始化。
        static constexpr int constexprValue = 20;  // 直接在類別内初始化。
    public:
        static int staticValue; // 宣告 static 成員變數
    };
    
    // 在類別外部定義並初始化 static 成員變數
    int MyClass::staticValue = 0;
    
    int main() {
        // 存取並修改 static 成員變數
        MyClass::staticValue = 5;
        std::cout << "Static Value: " << MyClass::staticValue << std::endl;
    
        // 透過實例存取 static 成員變數
        MyClass obj;
        std::cout << "Static Value through instance: " << obj.staticValue << std::endl;
    
        return 0;
    }
    • 因為 static 成員變數是獨立於任何物件,他們在程式啟動時就已經存在。並在程式終止時銷毀。

    • 其儲存位置不在任何類別的物件中,而是在全局變數區域。因此,除了上述的特殊情况,其需在類別的外部進行定義和初始化。

    • 存取 『static 成員變數(函數)』:

      • 類別::成員變數(函數)

      • 物件. 成員變數(函數)

  • 生命週期與儲存類型

    • 儲存期(Storage Duration):一般 static 成員變數具有靜態儲存期(Static Storage Duration),其儲存空間於整個程式執行期間持續存在,不受個別類別物件的建立與銷毀影響。

    • 生命週期(Object Lifetime):靜態成員的儲存期與物件生命週期並非完全相同。對於需要動態初始化的類別型別,物件的建構時機不一定等同於程式啟動時刻。

    • 記憶體配置:在一般執行環境中,static 成員變數通常配置於程式的靜態資料區,例如 .data、.bss 或唯讀資料區,而不是隨個別物件配置在 Stack 或 Heap 中。

    • 特殊情況:若靜態資料成員同時宣告為 thread_local,則具有執行緒儲存期(Thread Storage Duration),每個執行緒各自擁有一份資料,而不是整個程式共用同一份。

static 成員函數(Static Member Function)

  • 定義:成員函數可以使用 static 關鍵字修飾,稱為『static 成員函數(靜態成員函數)』。

  • 類別層級:static 成員函數屬於『類別』本身,而『非』任何特定物件的函數,因此不依賴特定物件實例。

  • 獨立於物件實例:static 成員函數可以在『沒有建立任何物件』的情況下直接呼叫。

  • 呼叫方式:

    • 通常透過『類別名稱』搭配範圍解析運算子 :: 呼叫,例如 ClassName::functionName()。

    • 也可以透過『物件』呼叫,但建議使用類別名稱,以明確表達其屬於類別的特性。

  • 無 this 指標:

    • static 成員函數沒有隱含的 this 指標。

    • this 指標用於指向目前執行非靜態成員函數的物件,而 static 成員函數不與任何特定物件綁定,因此無法使用 this。

  • 成員存取限制:

    • 可以直接存取類別的『static 成員變數』及呼叫其他『static 成員函數』(仍須符合存取權限)。

    • 無法直接存取類別的『非 static 成員變數』或呼叫『非 static 成員函數』,因為這些成員需要透過特定物件進行操作。

    • 但若明確提供『物件、參考(Reference)或指標(Pointer)』,則仍可透過該物件存取『非 static 成員』。

    • static 成員函數也可以操作自身的區域變數、函數參數,以及其他可存取的全域變數或函數。

  • 記憶體特性:

    • 呼叫 static 成員函數時,不需要事先建立物件,也不需要傳遞隱含的 this 指標。

    • 無論是一般成員函數或 static 成員函數,其函數程式碼通常都由所有物件共用,不會因建立多個物件而產生多份函數程式碼。

    • 因此,使用 static 成員函數的主要目的並非節省記憶體,而是表達『該函數不依賴特定物件』的設計語義。

  • 常見用途:

    • 物件數量統計:搭配 static 成員變數,管理或查詢目前的物件數量。

    • 工具函數(Utility Function):執行不依賴特定物件狀態的共用功能。

    • 型別轉換與數學計算:提供不需要物件實例的轉換邏輯或計算公式。

    • 工廠函數(Factory Function):提供建立或取得類別物件的共用介面。

核心觀念:static 成員函數與一般非靜態成員函數的主要差異,在於是否具有隱含的 this 指標,以及是否依賴特定物件實例。

  • #include <iostream>
    using namespace std;
    
    class MyClass {
    private:
        static int count;   // 靜態成員變數(全類別共享)
    
    public:
        MyClass() { ++count; }
        ~MyClass() { --count; }
    
        // 靜態成員函數:無 this 指標
        static void showCount() {
            cout << "Current object count: " << count << endl;
    
            // cout << value;   // 錯誤:無法存取非 static 成員
            // foo();           // 錯誤:無法呼叫非 static 函數
        }
    
        void foo() {
            cout << "Instance function" << endl;
        }
    };
    
    // 在類別外定義 static 成員變數
    int MyClass::count = 0;
    
    int main() {
        MyClass a, b;
    
        // 透過類別呼叫
        MyClass::showCount();
    
        // 透過物件呼叫(合法但不建議)
        a.showCount();
    
        return 0;
    }
  • 『static 成員函數』的實作不需在前面加上 static 關鍵字。

    • static 關鍵字在宣告時用於指定該成員函数屬於類別本身,而非屬於類別的某個特定實例。

    • 一旦宣告後,這個屬性就已經被確定,故在類別外部實作該函數時,則不需要重複指定(否則會發生編譯錯誤)。

    class MyClass {
    public:
        static void staticFunction(); // 在類別內宣告 static 成員函数
    };
    
    // 在類別外部定義 static 成員函数時,不需要再次使用 static 關鍵字。
    void MyClass::staticFunction() {
        // 函数實作
    }
    #include <iostream>
    
    class MyClass {
    public:
        static int staticValue;
    
        static void displayStaticValue() {
            std::cout << "Static Value: " << staticValue << std::endl;
        }
    };
    
    int MyClass::staticValue = 0;
    
    int main() {
        MyClass::staticValue = 5;
        MyClass::displayStaticValue();  // 透過類別名呼叫 static 成員函數
    
        return 0;
    }
    // MyTime.h
    
    #ifndef __MyTime___
    #define __MyTime___
    
    #include <iostream>
    
    class Time
    {
    public:
        static int mystatic;                // 宣告靜態成員變數但沒有定義。
        mutable int Hour;
        int Minute;
        int Second;
        explicit Time();
        Time(int tmphour);           
        explicit Time(int tmphour, int tmpmin, int tmpsec);
        void addHour(int tmphour)
        {
            Hour += tmphour;
        }
        void noone() const
        {
            Hour += 3;
        }
        Time& rtnhour(int tmphour);
        Time& rtnminute(int tmpminute);
        static void mstafunc(int testvalue);
    
    private:
        int Millisecond;
        void initMilliTime(int mls);
    };
    
    #endif /* __MyTime___ */
    // MyTime.cpp
    
    #include "MyTime.h"
    
    int Time::mystatic = 100;
    
    Time::Time(int tmphour, int tmpmin, int tmpsec) // 成員函數
    {
        Hour = tmphour;
        Minute = tmpmin;
        Second = tmpsec;
        initMilliTime(0);
    }
    
    Time::Time()
    {
        Hour = 12;
        Minute = 59;
        Second = 59;
        initMilliTime(59);
    }
    
    Time::Time(int tmphour)
    {
        Hour = tmphour;
        Minute = 59;
        Second = 59;
        initMilliTime(59);
    }
    
    void Time::initMilliTime(int mls)
    {
        Millisecond = mls;
    }
    
    Time& Time::rtnhour(int tmphour)
    {
        Hour += tmphour;
        return *this;
    }
    
    Time& Time::rtnminute(int tmpminute)
    {
        Minute += tmpminute;
        return *this;
    }
    
    void Time::mstafunc(int testvalue)
    {
        // Minute = testvalue;              // 錯誤。
        mystatic = testvalue;               // 正確。
    }
    // test.cpp
    
    #include <iostream>
    #include "utilities.h"
    #include "MyTime.h"
    
    // int Time::mystatic = 500;
    
    int main(int argc, char const *argv[])
    {
      Time::mstafunc(1288);
      std::cout << Time::mystatic << std::endl;
      Time mytime1;
      mytime1.mstafunc(2000);                           // 也可以用物件名.靜態成員函數名來呼叫成員函數。
      std::cout << Time::mystatic << std::endl;       
      return 0;
    }
    #include <iostream>
    using namespace std;
    
    class Counter {
    private:
        static int objectCount;  // 靜態變數
    public:
        Counter() {
            objectCount++;
        }
    
        ~Counter() {
            objectCount--;
        }
    
        static int getObjectCount() {
            return objectCount;
        }
    };
    
    // 初始化靜態變數
    int Counter::objectCount = 0;
    
    int main() {
        Counter obj1, obj2;
    
        cout << "Number of objects: " << Counter::getObjectCount() << endl;
    
        {
            Counter obj3;
            cout << "Number of objects: " << Counter::getObjectCount() << endl;
        }  // obj3 被銷毀
    
        cout << "Number of objects: " << Counter::getObjectCount() << endl;
    
        return 0;
    }
    #include <iostream>
    #include <cmath>
    using namespace std;
    
    class MathUtils {
    public:
        static double square(double x) {
            return x * x;
        }
    
        static double sqrt(double x) {
            return std::sqrt(x);
        }
    };
    
    int main() {
        cout << "Square of 4: " << MathUtils::square(4) << endl;
        cout << "Square root of 16: " << MathUtils::sqrt(16) << endl;
    
        return 0;
    }
    #include <iostream>
    using namespace std;
    
    class Example {
    private:
        int nonStaticVar = 10;  // 非靜態變數
        static int staticVar;  // 靜態變數
    public:
        static void staticFunction() {
            // cout << nonStaticVar; // 錯誤:無法存取非靜態變數
            cout << "StaticVar: " << staticVar << endl; // 正確:可以存取靜態變數
        }
    };
    
    int Example::staticVar = 42;
    
    int main() {
        Example::staticFunction();
        return 0;
    }
類別 QuantLib 使用位置 說明
Settings Settings::instance() Singleton 實作
Date Date::isLeap(year) 靜態工具函數
Actual360 static const Actual360 共用常數日數計算器
CashFlow CashFlow::basisPointValue(...)

靜態績效計算函數。

CashFlows 類別本身就像是一個「功能集合(function collection)」的工具類。

需求 是否該用 static 原因
所有物件共用 ✅ 共用資源
不需要存取成員變數 ✅ 工具邏輯
只想建立一個實例 ✅ 單例模式
定義類別級常數 ✅ 常用定義

static 與一般成員函數的物件呼叫差異

比較項目 一般非 static 成員函數 static 成員函數
是否依賴特定物件 是 否
是否具有隱含 this 指標 是(一般成員函數) 否
使用 ClassName::func() 直接呼叫 不可 可以
使用 obj.func() 呼叫 可以 可以
使用 ptr->func() 呼叫 可以 可以
是否需要建立物件才能呼叫 一般需要 不需要
是否能直接存取當前物件的非靜態成員 可以 不可以
是否能直接存取靜態成員 可以 可以
#include <iostream>
using namespace std;

class Example {
private:
  int value = 10;
  inline static int count = 0;  // C++17
  
public:
  // 一般非 static 成員函數
  void normalFunction() {
    cout << value << endl;
    cout << this->value << endl;
  }
  
  // static 成員函數
  static void staticFunction() {
    count++;
    cout << count << endl;
    
    // cout << value;       // Error
    // cout << this->value; // Error
  }
  
  // static 函數透過指定物件存取非 static 成員
  static void showValue(const Example& obj) {
    cout << obj.value << endl;  // OK
  }
};

int main() {
  // 1. 透過類別名稱呼叫
  Example::staticFunction(); // OK
  
  // Example::normalFunction(); // Error
  
  // 2. 建立物件
  Example obj;
  
  // 3. 透過物件呼叫
  obj.normalFunction(); // OK
  obj.staticFunction(); // OK
  
  // 4. 透過物件指標呼叫
  Example* ptr = &obj;
  
  ptr->normalFunction(); // OK
  ptr->staticFunction(); // OK
  
  // 5. static 函數透過指定物件存取非 static 成員
  Example::showValue(obj);
  
  return 0;
}
  • 重點說明:

    1. 一般非 static 成員函數:

      • 必須透過特定物件進行一般的成員函數呼叫。

      • 呼叫時具有隱含的 this 指標,指向執行該函數的物件。

      • 可以直接存取該物件的非 static 成員變數與函數。

    2. static 成員函數:

      • 可以透過『類別名稱』直接呼叫,不需要建立物件。

      • 也可以透過『物件』呼叫,但該物件不會成為函數的隱含參數。

      • 不論透過哪個物件呼叫,都不會產生對應於該物件的 this 指標。

      • 若需要操作特定物件的非 static 成員,必須明確指定該物件。

    3. 兩種呼叫方式的差異:

      • Example::staticFunction():直接透過類別呼叫,不需要物件實例。

      • obj.staticFunction():語法上透過物件呼叫,但函數本身仍不依賴 obj 的狀態。

      • 兩者呼叫的是同一個 static 成員函數,不會因使用不同物件而產生不同版本的函數。

      • 特別注意:透過物件運算式呼叫時,該運算式仍可能被求值,因此不能理解為物件運算式一定會被完全忽略。

    • 記憶體特性:

      • 呼叫 static 成員函數時,不需要傳遞隱含的 this 指標。

      • 無論是一般成員函數或 static 成員函數,其函數程式碼通常都由所有物件共用,不會因建立多個物件而產生多份函數程式碼。

      • 使用 static 成員函數的主要目的並非節省記憶體,而是表達『該函數不依賴特定物件』的設計語義。

    核心觀念:一般非 static 成員函數的執行依賴特定物件;static 成員函數則不依賴特定物件。即使 static 成員函數可以透過物件呼叫,也不代表該函數具有 this 指標。

類別初始化、預設建構函數、“= default;”、“= delete;”

與類別相關的函數

  • 若有些功能函數,與某類別相關,但又不需要定義在類別中,例如:打印某個成員變數之值。

  • 則該函數的定義可放在該『類別成員函數實作』的程式碼內(.cpp)。

    #include <iostream>
    // 普通函數
    void WriteTime(Time& mytime)
    {
      std::cout << mytime.Hour << std::endl;
    }
  • 函數宣告寫在該類別的標頭檔(.h)。

    #include "MyTime.h"
    
    int Time::mystatic = 100;
    
    Time::Time(int tmphour, int tmpmin, int tmpsec) // 成員函數
    {
        Hour = tmphour;
        Minute = tmpmin;
        Second = tmpsec;
        initMilliTime(0);
    }
    
    Time::Time()
    {
        Hour = 12;
        Minute = 59;
        Second = 59;
        initMilliTime(59);
    }
    
    Time::Time(int tmphour)
    {
        Hour = tmphour;
        Minute = 59;
        Second = 59;
        initMilliTime(59);
    }
    
    void Time::initMilliTime(int mls)
    {
        Millisecond = mls;
    }
    
    Time& Time::rtnhour(int tmphour)
    {
        Hour += tmphour;
        return *this;
    }
    
    Time& Time::rtnminute(int tmpminute)
    {
        Minute += tmpminute;
        return *this;
    }
    
    void Time::mstafunc(int testvalue)
    {
        // Minute = testvalue;              // 錯誤。
        mystatic = testvalue;               // 正確。
    }
    
    
    // 普通函數 /////////////////////////////////////
    void WriteTime(Time& mytime)
    {
      std::cout << mytime.Hour << std::endl;
    }
    #include <iostream>
    #include "utilities.h"
    #include "MyTime.h"
    
    int main(int argc, char const *argv[])
    {
      Time mytime(12, 15, 17);
      WriteTime(mytime);
      return 0;
    }

類別內初始值

  • C++ 11 標準之後,可為新成員變數提供類別內的初始值。而對於沒有初始值的成員變數,系統會有預設的初始化策略,例如對於 int 型態的成員變數,為『未確定值』。

  • 若使用『建構函數初始化列表』則會對初始值覆蓋。

const 成員變數的初始化

  • 對於類別內的 const 成員,如之前講義內容所述,只能使用『初始化列表』來進行初始化,而無法使用建構函數函數體內部進行賦值操作。

    // MyTime.h
    // Time類別內部加入以下內容:
    const int testvalue;               
    
    
    // MyTime.cpp的建構函數中,程式碼應改為:
    Time::Time(int tmphour, int tmpmin, int tmpsec)
              :Hour(tmphour), Minute(tmpmin), testvalue(18)              // 『初始化列表』
    {
      // testvalue = 6;                     // 錯誤:不可在這裡初始化
      // ..................
    }
  • 若類別內的成員變數為『參考』,必須對其進行初始化,故必須使用『初始化列表』來進行初始化,而無法使用建構函數函數體內部進行賦值操作。

預設建構函數

  • 如前述,類別可以有多個建構函數,其中,『無參數的建構函數』稱為『預設建構函數』。

  • 若類別沒有建構函數,則編譯器會產生一個『合成預設建構函數』(在滿足一些的情形之下),但無論如何,都能產生物件。

  • 注意:一但程式設計師定義了自己的『建構函數』,則不管這個建構函數帶有幾個參數,編譯器就不會創建『合成預設建構函數』。

=default;

  • 在 C++ 中,=default 是一種特殊的語法,用於明確告訴編譯器:為某個函數進行『預設』的實作。

  • 一般用於『建構函數』、『解構函數(deconstructor)』與『賦值運算子』。

  • 使用 =default; 可以實現幾個目的:

    • 強調使用『預設』行為:透過 =default;,可明確表示希望使用編譯器提供的『預設行為』,而非程式設計師提供。

    • 在 C++11 及以後的版本中,若定義自定義的建構函數或解構函數,編譯器將不會自動產生預設的複製建構函數或複製賦值運算子。使用 =default; 則可明確要求編譯器產生它們。

    • 提高類別的特殊成員函數的可用性:在某些情況下,你可能希望類別是可複製的或可預設建構的,且希望使用編譯器產生的版本。在這種情況下,=default 是非常有用的。

  • 在預設構造函數宣告的末尾,與分號; 之前加入 =default。

  • 編譯器能夠為該類函數自動產生『空函數體』(等價於 {})。

  • 不需自己寫『預設建構函數的函數體』。

  • 不適用於普通的成員函數。

  • 只適用『無參數』的預設建構函數。

    • =default; 通常是為了讓編譯器為特殊成员函数(如預設建構函數、複製建構函數、複製赋值運算子、移動建構函数和移動赋值運算子等)提供預設的實作。

    • 然而,對於普通、帶參數的建構函数,使用 =default; 為不適用,也不被允許。

      class MyClass {
      public:
          MyClass(int x) = default; // 錯誤:無法為帶參數的普通建構函數使用=default;。
      };
    class MyClass {
    public:
        MyClass() = default;  // 預設建構函數
        ~MyClass() = default; // 預設解構函數
        MyClass(const MyClass&) = default; // 預設複製建構函數
        MyClass& operator=(const MyClass&) = default; // 預設複製賦值運算子
    };
    • MyClass 類別的所有特殊成員函數都被明確地設置為『預設實作』。

    • 意指即使為該類別定義了其他建構函數,編譯器仍會提供這些預設的特殊成員函數。

=delete;

  • 顯性地禁止編譯器自動產生某個函數的預設動作。

  • 例如:Time2()=delete;,這意味著你不能透過無參數的方式創建Time2物件,並強制要求使用者提供必要的參數來初始化物件。這對於強制某些類別物件應該始終具有特定初始狀態是很有用的。

    class Time2 {
    public:
        Time2(int hours, int minutes) : hours_(hours), minutes_(minutes) {}
    
        // 禁用合成默認構造函數
        Time2() = delete;
    
        void printTime() {
            std::cout << hours_ << ":" << minutes_ << std::endl;
        }
    
    private:
        int hours_;
        int minutes_;
    };
    
    int main() {
        // 正確,使用自定義構造函數初始化物件
        Time2 t1(10, 30);
        t1.printTime();
    
        // 編譯錯誤,無法使用合成默認構造函數
        Time2 t2; // 此行將導致編譯錯誤
    
        return 0;
    }

重載運算子(Operator Overloading)

  • 定義:運算子重載(Operator Overloading)允許程式設計者替『自訂類別』定義既有運算子的行為,使類別物件可以使用類似內建型別的運算方式。

  • 常見運算子:

    • 比較運算子:==、!=、>、>=、<、<=。

    • 算術運算子:+、-、*、/、%。

    • 賦值運算子:=、+=、-=、*=、/=。

    • 其他運算子:[]、()、++、--、<<、>> 等。

  • 自訂運算行為:

    • C++ 內建型別通常具有既定的運算行為,例如整數可以直接使用 == 比較數值是否相等。

    • 但對於自訂類別,編譯器不一定知道應該如何比較兩個物件。

    • 因此,可以透過『運算子重載』,明確定義類別物件執行特定運算時所應具有的行為。

  • 相等比較(Equality Comparison):

    • 若希望使用 == 比較兩個自訂類別物件,可以定義 operator==(),指定物件相等的判斷條件。

    • 若類別未提供可使用的 operator==(),且不存在其他適用的比較方式(例如隱含型別轉換),則使用 == 比較兩個物件會發生編譯錯誤。

    • C++20 起,可以使用 = default,要求編譯器依據成員逐一比較的規則產生『相等比較運算子(==)』。

      
      #include <iostream>
      
      class Bond {
      private:
        int id_;
        double faceValue_;
      
      public:
        Bond(int id, double faceValue)
          : id_(id), faceValue_(faceValue) {}
      
        // C++20 自動產生『 == 相等比較運算子』
        bool operator==(const Bond& other) const = default;
      };
      
      int main() {
        Bond b1(101, 1000.0);
        Bond b2(101, 1000.0);
        Bond b3(102, 1000.0);
      
        std::cout << std::boolalpha;
      
        std::cout << (b1 == b2) << "\n";
        std::cout << (b1 == b3) << "\n";
        std::cout << (b1 != b3) << "\n";
      
        return 0;
      }
  • 運算子重載的限制:

    • 只能重載 C++ 已經存在且允許重載的運算子,不能自行創造新的運算子。

    • 不可以改變運算子的優先順序、結合性或運算元數量。

    • 重載的運算子至少有一個運算元必須是類別或列舉型別,不能改變內建型別彼此運算的原有行為。

    • 並非所有 C++ 運算子都能重載,例如 ::、.、.*、?: 等不能重載。

  • 運算子重載應符合運算子原本的語義。例如 == 應表達物件是否相等,而不是執行加法或修改物件狀態。否則,即使語法合法,也會降低程式的可讀性與可維護性。

  • 例如,對基本型別而言:

int a = 10;
int b = 10;

cout << (a == b);

編譯器本來就知道兩個 int 應該如何進行相等比較。

但是對自訂類別:

class Money {
public:
    double value;
};

如果寫:

Money m1;
Money m2;

if (m1 == m2) {
    // ...
}

若類別沒有定義適當的 == 運算子,編譯器並不知道:

「兩個 Money 物件在什麼條件下才算相等?」

因此會產生編譯錯誤。

// Time.h
bool MyTime::operator==(Time&t);
// Time.cpp
bool MyTime::operator==(Time&t)
{
  if(Hour == t.Hour)
    return true;
  return false;
}
  • 重載運算子本質上為函數,函數的正式名稱為:operator關鍵字後面接運算子。

  • Date 類別的比較運算子(ql/time/date.hpp):

    • bool operator<(const Date& d1, const Date& d2);
    • bool operator==(const Date& d1, const Date& d2);
  • InterestRate 的相等比較(ql/interestrate.hpp):

    • bool operator==(const InterestRate& r1, const InterestRate& r2);
    #include <iostream>
    using namespace std;
    
    class Money {
    public:
      double value;
    
      Money(double v)
        : value(v) {
      }
    
      bool operator==(const Money& other) const {
        return value == other.value;
      }
    };
    
    
    int main() {
      Money m1(100.0);
      Money m2(100.0);
      Money m3(200.0);
    
      cout << (m1 == m2) << endl;   // 1
      cout << (m1 == m3) << endl;   // 0
    
      return 0;
    }

複製賦值運算子

  • 定義:複製賦值運算子是用來將一個物件的內容,複製並賦值給另一個『已經存在的物件』。

  • 運算子重載:複製賦值運算子是 operator= 的一種形式,可由程式設計者自行定義類別物件的複製賦值行為。

  • 一般形式為:

ClassName& operator=(const ClassName& other);
  • 語法說明:

    • operator=:重載賦值運算子 =,定義物件的複製賦值行為。

    • const ClassName& other:以『常數參考』接收來源物件,避免不必要的物件複製,並防止透過該參考修改來源物件。

    • ClassName&:回傳『目前物件的參考』,使賦值運算可以連續使用,例如 a = b = c;。

    • return *this;:透過 this 指標取得目前物件,再使用 *this 取得該物件本身,並以參考形式回傳。

  • 範例:

    Time myTime;                // 呼叫預設建構函數(不帶參數)。
    Time myTime2 = myTime       // 呼叫複製建構函數。
    Time myTime5 = {myTime};    // 呼叫複製建構函數。
    
    Time myTime6;               // 呼叫預設建構函數(不帶參數)。
    myTime6 = myTime5;          // 賦值運算子,並非呼叫複製建構函數。
    class Money {
    public:
      double value;
    
      Money& operator=(const Money& other) {
        value = other.value;
        return *this;
      }
    };
    
    Money m1;
    Money m2;
    
    m1.value = 100.0;
    m2.value = 200.0;
    
    m2 = m1;       // 會呼叫:m2.operator=(m1);
  • 若對物件賦值,系統會呼叫一個複製賦值運算子。

  • 複製建構函數與複製賦值運算子的差別

    • 目標物件是否已經存在。

      • Money m1(100.0); Money m2 = m1;

        此時 m2 原本不存在,是利用 m1 建立新的物件 m2。

        因此呼叫:

        Money(const Money& other);

        這是:

        Copy Constructor(複製建構函數)

      • Money m1(100.0); Money m2(200.0);

        m2 = m1;

        此時:

        m2

        早就已經存在。

        現在只是把 m1 的內容複製到 m2。

        因此呼叫:

        Money& operator=(const Money& other);

        這是:

        Copy Assignment Operator(複製賦值運算子)

    • Self-assignment(自己指定給自己)?? m1 = m1;
      會發生什麼事?什麼情況編譯會過?什麼情況會有問題?會有什麼風險? (HW)

  • 若未重載複製賦值運算子,編譯器會使用預設的物件賦值規則為物件賦值。

    Time& operator=(const Time&);            // 賦值運算子重載。
  • 編譯器自動產生的複製賦值運算子

    • 如果類別沒有自行宣告複製賦值運算子,編譯器通常會隱含宣告一個複製賦值運算子。

    • 當符合條件時,編譯器會產生其實作,依序對基底類別與非靜態資料成員進行賦值。

    • 在某些條件下,隱含的複製賦值運算子可能被定義為已刪除(Deleted),因此不是所有類別都能進行複製賦值。

    • C++11 起,也可以使用 = default 明確要求編譯器產生預設的複製賦值運算子。

    class Student {
    public:
      string name;
      int score;
    
      Student& operator=(const Student&) = default;
    };

解構函數(釋放函數)

  • 解構函數(destructor)與建構函數是相對的。當物件被銷毀或刪除時,系統會呼叫解構函數。

  • 解構函數的主要目的是釋放物件所持有的資源,如動態分配的記憶體、打開的檔案、網絡連線等,以確保程式在結束時不會造成資源洩漏或記憶體洩漏。

  • 解構函數也沒有回傳值。

  • 若不定義自己的解構函數,編譯器則可能會產生一個『預設解構函數』。

  • 解構函數的命名方式是在類別名稱前加上『波浪號(~)』,且不帶參數。例如:

    class MyClass {
    public:
        MyClass() {
            // 構造函數
        }
    
        ~MyClass() {
            // 解構函數
        }
    };
    // Time.h
    // Time類別內部,宣告public修飾的解構函數
    
    pubic:
      ~Time();                          // 宣告Time類別的解構函數。
    // Time.cpp
    Time::~Time()
    {
      // 程式碼
      int abc;
      abc = 0;
    }
  • 當釋放一個物件時,首先執行該物件所屬類別的解構函數的函數體,執行完畢後,該物件就會被銷毀。

  • 銷毀時,『先』定義的物件數會『後』銷毀。

  • 若是採 malloc/new 分配的記憶體空間,則一般需手動釋放(透過在解構函數中使用 free/delete)。

  • 解構函數在以下情況自動被調用:

    • 程式退出時:當整個程式結束執行時,所有全局物件的解構函數將被自動調用。

    • 物件超出作用域:當局部物件超出其作用域(例如,離開函數區塊)時,它的解構函數將被自動調用。

    • delete 運算子:當使用 delete 運算子釋放動態分配的物件時,該物件的解構函數將被自動調用。

    • 容器類的元素:當物件是容器(如標準程式庫中的std::vector或std::list)的元素時,當容器被銷毀或元素被刪除時,該元素的解構函數將被自動調用。

    class MyResource {
    public:
        MyResource() {
            // 分配資源
        }
    
        ~MyResource() {
            // 釋放資源
        }
    };
    
    int main() {
        MyResource resource; // 創建物件
    
        // 执行其他操作
    
        // 在main函数結束時,MyResource物件的解構函數將被自動調用,
        // 以釋放分配的資源
        return 0;
    } // MyResource物件在這裡超出了作用域,解構函數被自動調用
    #include <iostream>
    using namespace std;
    
    class Example {
    private:
        int* data;
    public:
        // Constructor
        Example(int value) {
            data = new int(value); // 分配動態記憶體
            cout << "Constructor: Memory allocated for " << *data << endl;
        }
    
        // Destructor
        ~Example() {
            delete data; // 釋放動態記憶體
            cout << "Destructor: Memory deallocated" << endl;
        }
    
        void printValue() const {
            cout << "Value: " << *data << endl;
        }
    };
    
    int main() {
        Example obj(42); // Constructor called
        obj.printValue();
        // Destructor will be called automatically here when obj goes out of scope
        return 0;
    }
    #include <iostream>
    #include <fstream>
    using namespace std;
    
    class FileHandler {
    private:
        ofstream file;
    public:
        FileHandler(const string& filename) {
            file.open(filename);
            if (file.is_open()) {
                cout << "File opened: " << filename << endl;
            }
        }
    
        ~FileHandler() {
            if (file.is_open()) {
                file.close();
                cout << "File closed." << endl;
            }
        }
    
        void write(const string& data) {
            if (file.is_open()) {
                file << data << endl;
            }
        }
    };
    
    int main() {
        FileHandler handler("example.txt");
        handler.write("Hello, world!");
        // File will be closed automatically when handler goes out of scope
        return 0;
    }
    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        Base() {
            cout << "Base constructor" << endl;
        }
    
        ~Base() {
            cout << "Base destructor" << endl;
        }
    };
    
    class Derived : public Base {
    public:
        Derived() {
            cout << "Derived constructor" << endl;
        }
    
        ~Derived() {
            cout << "Derived destructor" << endl;
        }
    };
    
    int main() {
        Derived obj;
        return 0;
    }

子類別、呼叫順序,存取等級與函數『覆寫(override)』

  • 當有父類別(基礎類別)、子類別(衍生類別)時,這種層次關係為『繼承』,也就是子類別能夠從父類別那裡繼承到許多東西,包括成員變數和成員函數。

  • 先有父類別。父類別主要定義一些『公用』的成員變數與成員函數。接著透過『繼承』此父類別建立新的類別(子類別)。

  • 定義子類別時則可減少許多撰寫程式碼的工作量:只需撰寫子類別獨有的部分即可。

  • 定義子類別語法:class 子類別名:繼承方式 父類別名

  • 子類別可以繼承多個父類別(多重繼承)。

    // Human.h
    #ifndef __Human__
    #define __Human__
    
    class Human
    {
        public:
            Human();
            Human(int);
        public:
            int m_Age;
            char m_Name[100];
    };
    
    #endif /* __Human__ */
    // Humen.cpp
    #include "Human.h"
    #include <iostream>
    
    Human::Human()
    {
        std::cout << "執行Human::Human()建構函數" << std::endl;
    }
    
    Human::Human(int age)
    {
        std::cout << "執行Human::Human(int age)建構函數" << std::endl;
    }
    // Men.h
    #ifndef __Men__
    #define __Men__
    
    #include "Human.h"
    
    class Men:public Human
    {
        public:
            Men();
    };
    
    #endif /* __Men__ */
    // Men.cpp
    #include "Men.h"
    #include <iostream>
    
    Men::Men()
    {
        std::cout << "執行Men::Men()建構函數" << std::endl;
    
    }
    // test.cpp
    #include <iostream>
    #include "Men.h"
    
    int main(int argc, char const *argv[])
    {
      Men men;
      return 0;
    }
    • 程式會先執行『父類別』的建構函數,再執行『子類別』的建構函數。

存取等級(public、protected、private)

  • public:可以被任意物件存取。

  • protected:只允許本類別或『子類別』的成員函數存取。

  • private:只允許本類別的成員函數存取。

繼承等級(public、protected、private)

  • 子類別 public 繼承父類別:父類別所有成員在子類別的存取權限『不會』發生改變。

  • 子類別 protected 繼承父類別:將父類別中 public 成員變成子類別的 protected 成員。父類別中的 protected 成员仍然是子類別的 protected 成員。

  • 子類別 private繼承父類別:使得父類別所有成員在子類別中的存取權限變為 private。

  • 父類別的 private 成員不受繼承方式的影響,『子類別永遠無權存取』。

  • 因此,對於父類別而言,尤其是父類別的成員函數,若不想被外界存取,則設定為 private;若想讓自己的子類別存取,則應設定為 protected。若想公開,則設定為 public。

    #ifndef __Human__
    #define __Human__
    
    class Human
    {
        public:
            Human();
            Human(int);
        public:
            int m_Age;
            char m_Name[100];
    
        protected:
            int m_prol;
            void funcpro() {};
    
        private:
            int m_privl;
            void funcpriv();
    };
    
    #endif /* __Human__ */
    • 若 Men 類別 protected 繼承 Humen 類別:
    class Men:protected Human
    {
      //  
    };
    
    Men men;
    men.m_Age = 10;                     // 錯誤:不允許main函數直接存取。   
    men.m_privl = 15                    // 錯誤:子類別無存取權限。
  • 子類別可以存取父類別中的 public 和 protected 成員,並可以進一步將它們公開或保護。

  • 子類別無法直接存取父類別中的 private 成員,但可以使用父類別提供的公共函數存取這些私有成員。

  • 子類別可以『覆寫(override)』父類別的 public 和 protected 函數,並可以更改函數的存取等級,但必須符合覆寫的規則。

    • 函數覆寫(override):

      • 子類別可以使用與父類別相同的函數名稱和參數來覆寫父類別的函數。這樣一來,當透過子類別的物件呼叫該函數時,會執行子類別的版本而非父類別的版本。

      • 條件:

        • 父類別的成員函數是『虛函數(virtual)』。

        • 名稱、參數列表、回傳型態(需相容)都要完全相同。

        • 在子類別中實作時,代表「重新定義父類別的行為」。

      • 規則:

        • 函數『名稱』、參數的『型別』及『順序』、『回傳型別』都必須完全相同,否則『不會』被認為是覆寫。

        • 要達到「覆寫(override)」效果,父類別的成員函數必須是 virtual。否則即使子類別定義了同名、同參數、同回傳型態的函數,C++ 也只會視為「遮蔽(hiding)」,而不是覆寫。

        • 當父類別中的函數被標記為 virtual,則覆寫時建議使用 override 關鍵字以表明覆寫的意圖,則編譯器可進行檢查,避免覆寫錯誤。

        • override 是 C++11 引入的『編譯期檢查』關鍵字。若父類別沒有 virtual,但你用了 override,會直接編譯錯誤:

        • 如果參數或回傳型態有任何差異,C++ 不會認為這是覆寫,而可能視為『函數遮蔽(Function Hiding)』。

      class Parent {
      protected:
          int protectedMember; // 父類別的 protected 成員
      public:
          void setProtectedMember(int value) {
              protectedMember = value; // 在父類別中可以存取 protected 成員
          }
      };
      
      class Child : protected Parent {
      public:
          void accessProtectedMember() {
              // 子類別可以存取從父類別繼承的 protected 成員
              protectedMember = 42;
          }
      };
      
      int main() {
          Child child;
          // 外部程式码無法直接存取子類別的 protected 成員
          // child.protectedMember = 42; // 編譯錯誤
      
          child.accessProtectedMember(); // 透過子類別的公共函數存取 protected 成員
      
          return 0;
      }
      • Child 類別使用 protected 繼承 Parent 類別繼承了protectedMember成員。

      • 子類別中的 accessProtectedMember 函数可以直接存取 protectedMember,而外部程式碼無法直接存取子類別的 protected 成員。

  • 公有繼承(public)

    • 概念:子類別可以存取父類別的公有成員和保護成員,但無法直接存取私有成員。子類別對父類別的公有成員繼承保持相同的存取權限(即公有成員仍為公有,保護成員仍為保護)。

    • 常見場景:這是『最常見』的繼承方式,通常用於「是種關係」的情況,即子類別是父類別的一種類型。比如,Dog 是 Animal 的一種,這樣的繼承關係一般使用公有繼承。

  • 私有繼承(private)

    • 概念:子類別對父類別的公有和保護成員進行私有化,這意味著子類別中的父類別成員無法被外部程式碼直接存取(即使是子類的實例也無法直接存取)。

    • 常見場景:私有繼承通常用於需要使用父類別的功能或實現某些接口,但不希望讓外部程式碼直接與父類別互動的情況。例如,當你希望在子類別內部使用父類別的一些實現(例如,方法或資料),但不希望讓外部程式碼直接存取這些父類別的成員時,會選擇私有繼承。

  • 保護繼承(protected)

    • 概念:子類別對父類別的公有和保護成員進行保護化,這意味著子類別的成員可以存取父類別的公有和保護成員,但這些成員對外部程式碼(包括子類的實例)來說是不可見的。這種繼承方式通常用來強調「是種關係」,但同時限制了外部對父類別成員的存取。

    • 常見場景:保護繼承適用於那些子類需要使用父類別成員,但同時希望限制外部對這些成員的存取的情況。例如,在繼承一個基礎類別時,子類別需要繼承並使用父類別的部分方法或資料,但不希望外部能夠直接修改這些成員。

函數覆寫(Function Override)

  • 子類別可以『覆寫(override)』父類別的 public 和 protected 函數,並可以更改函數的存取等級,但必須符合覆寫的規則。

    • 函數覆寫(override):

      • 子類別可以使用與父類別相同的函數名稱和參數來覆寫父類別的函數。這樣一來,當透過子類別的物件呼叫該函數時,會執行子類別的版本而非父類別的版本。

      • 條件:

        • 父類別的成員函數是『虛函數(virtual)』。

        • 名稱、參數列表、回傳型態(需相容)都要完全相同。

        • 在子類別中實作時,代表『重新定義父類別的行為』。

      • 規則:

        • 函數『名稱』、參數的『型別』及『順序』、『回傳型別』都必須完全相同,否則『不會』被認為是覆寫。

        • 要達到「覆寫(override)」效果,父類別的成員函數必須是 virtual。否則即使子類別定義了同名、同參數、同回傳型態的函數,C++ 也只會視為「遮蔽(hiding)」,而不是覆寫。

        • 當父類別中的函數被標記為 virtual,則覆寫時建議使用 override 關鍵字以表明覆寫的意圖,則編譯器可進行檢查,避免覆寫錯誤。

        • override 是 C++11 引入的『編譯期檢查』關鍵字。若父類別沒有 virtual,但你用了 override,會直接編譯錯誤。

        • 加上 override 可強制編譯器檢查「是否真的覆寫」父類別虛函數,避免意外遮蔽。

        • 如果參數或回傳型態有任何差異,C++ 不會認為這是覆寫,而可能視為『函數遮蔽(Function Hiding)』。

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        virtual void show() { cout << "Base::show()\n"; }
    };
    
    class Derived : public Base {
    public:
        void show() override { cout << "Derived::show()\n"; } // 覆寫成功
    };
    
    int main() {
        Base* b = new Derived();
        b->show();   // 輸出:Derived::show()
    }

函數遮蔽 (Function Hiding)

  • 一般情況下,父類別的成員函數只要是用 public 或 protected 修飾的,子類別只要不使用 private 繼承父類別,則子類別都可以使用。

  • C++ 繼承中,子類別會『遮蔽(Hiding)父類別中的『同名函數』(不管函數的回傳值與參數)。

    • 名稱相同,但

    • 參數個數或型別不同

    • 或回傳型態不同但不相容

    • 或父函數不是 virtual

    #include <iostream>
    using namespace std;
    
    class Base {
    public:
        virtual void show() { cout << "Base::show()\n"; }
        virtual void show(int x) { cout << "Base::show(int)\n"; }
    };
    
    class Derived : public Base {
    public:
        void show(double x) { cout << "Derived::show(double)\n"; } // ⚠️ 遮蔽
    };
    
    int main() {
        Derived d;
        d.show(1.0);   // 呼叫 Derived::show(double)
        // d.show(1);  // 編譯錯誤,因為 Base::show(int) 被遮蔽掉了
        d.Base::show(1); // 可以用 Base:: 重新指定
    }
    // Human.h
    #ifndef __Human__
    #define __Human__
    
    class Human
    {
        public:
            Human();
            Human(int);
        public:
            int m_Age;
            char m_Name[100];
    
        protected:
            int m_prol;
            void funcpro() {};
    
        private:
            int m_privl;
            void funcpriv();
    
        public:
            void samenamefunc();
            void samenamefunc(int);
    };
    
    #endif /* __Human__ */
    // Human.cpp
    #include "Human.h"
    #include <iostream>
    
    Human::Human()
    {
        std::cout << "執行Human::Human()建構函數" << std::endl;
    }
    
    Human::Human(int age)
    {
        std::cout << "執行Human::Human(int age)建構函數" << std::endl;
    }
    
    void Human::samenamefunc()
    {
        std::cout << "執行Human::samenamefunc()建構函數" << std::endl;
    }
    
    void Human::samenamefunc(int)
    {
        std::cout << "執行Human::samenamefunc(int)建構函數" << std::endl;
    }
    // Men.h
    #ifndef __Men__
    #define __Men__
    
    #include "Human.h"
    
    class Men:protected Human
    {
        public:
            Men();
    
        public:
            void samenamefunc(int);
    };
    
    #endif /* __Men__ */
    // Men.cpp
    #include "Men.h"
    #include <iostream>
    
    Men::Men()
    {
        std::cout << "執行Men::Men()建構函數" << std::endl;
    
    }
    
    void Men::samenamefunc(int)
    {
        std::cout << "執行Men::samenamefunc(int)" << std::endl;
    
    }
    Men men;
    // men.samenamefunc();                // 錯誤:無法呼叫父類別不帶參數的samenamefunc函數
    men.samenamefunc(1);               // 正確:只能呼叫子類別具一個參數的samenamefunc函數,無法呼叫父類別帶一個參數的samenamefunc函數。
    
    // 執行Human::Human()建構函數
    // 執行Men::Men()建構函數
    // 執行Men::samenamefunc(int)
  • 若欲呼叫父類別的同名函數呢?可借助子類別的成員 samenamefunc 函數來呼叫父類別的成員函數 samenamefunc 函數。

    void Men::samenamefunc(int)
    {
      Human::samenamefunc();              // 可以使用父類別的無參數的smaenamefunc函數。
      Human::samenamefunc(120);           // 可以使用父類別的帶一個參數的smaenamefunc函數。
      std::cout << "執行void Men::samenamefunc(int)" << std::endl;
    }
    Men men;
    // men.samenamefunc();
    men.samenamefunc(1);
    
    // 執行Human::Human()建構函數
    // 執行Men::Men()建構函數
    // 執行Human::samenamefunc()建構函數
    // 執行Human::samenamefunc(int)建構函數
    // 執行void Men::samenamefunc(int)
  • 若子類別 Men 是以 public 繼承方式繼承 Human 父類別,則可在 main 主函數中使用:子類別物件名.父類別名::成員函數名(…):

    men.Human::samenamefunc();                   // 呼叫父類別中不帶參數的samenamefunc函數。
    men.Human::samenamefunc(160);                // 呼叫父類別中帶一個參數的samenamefunc函數。
  • 函數覆寫 (Function Overriding) 與 函數遮蔽 (Function Hiding) 的差異:

    函數覆寫 (Function Overriding) 函數遮蔽 (Function Hiding)
    發生情境 子類別定義了與父類別完全相同簽名(名稱、參數、回傳值)的虛函數。 子類別定義了與父類別名稱相同但參數、回傳值可能不同的函數。
    關鍵字要求 父類別函數需要使用 virtual 關鍵字,並在子類別中可選擇使用 override 明確表明覆寫行為(C++11 以上)。 不需要任何關鍵字,僅需在子類別中定義相同名稱的函數即可。
    影響範圍 覆寫的是父類別的虛函數,透過多態機制實現動態綁定。 子類別中具有相同名稱的函數會隱藏父類別中所有名稱相同的函數。
    是否動態綁定 是,透過虛函數表(Virtual Table)『動態綁定』。 否,函數遮蔽是『靜態綁定』,完全取決於編譯時的類型資訊。
    是否受參數影響 子類別函數簽名(名稱、參數、回傳值)需完全匹配父類別虛函數。 子類別函數名稱相同即可,與參數和回傳值無關。
    行為影響 覆寫後,當透過『父類別』指標或參考呼叫函數時,執行的是子類別版本的函數。 遮蔽後,子類別中的同名函數會隱藏父類別的所有同名函數,導致父類別的函數無法直接被子類別使用。
    使用情境 用於實現『多態』行為,讓子類別提供自己的實現來取代父類別的實現。 子類別定義的函數完全取代父類別的函數,或需要不同功能的實現。
  • 覆寫:強調「同一介面,多種實作」,適合框架、抽象類別、多態。依據物件「真實型別」決定執行哪個版本(動態繫結/綁定)

  • 遮蔽:強調「重新定義或額外重載」,適合提供新介面或額外功能,不需動態綁定。編譯期依據「指標/變數型別」決定呼叫哪個版本(靜態繫結/綁定)

  • 覆寫(override):父類別給你規格,子類別提供不同實作。

  • 遮蔽(hiding):子類別自訂新版本,不理會父類別舊規格。

#include <iostream>
using namespace std;

class Base 
{
public:
    virtual void foo()         { cout << "Base::foo()\n"; }
    virtual void bar(int x)    { cout << "Base::bar(int)\n"; }
};

class Derived : public Base 
{
public:
    void foo() override        { cout << "Derived::foo()\n"; } //  覆寫 (名稱與簽名完全相同)

    void bar(double x)         { cout << "Derived::bar(double)\n"; } //  遮蔽 (名稱相同但簽名不同)
};

int main() 
{
    Derived d;
    Base* bp = &d;

    bp->foo();      // Derived::foo()   (覆寫成功,動態綁定)
    bp->bar(10);    // Base::bar(int)   (父類別版本被呼叫)

    d.bar(10.5);    // Derived::bar(double)
    // d.bar(10);   // 編譯錯誤:Base::bar(int) 被遮蔽
}
  • foo():

    • 完全符合簽名 + 父類別是 virtual → 覆寫。

    • 透過 Base* 呼叫時,執行子類別版本。

  • bar():

    • 子類別提供 double 參數版本,與父類別 int 參數不同 → 遮蔽。

    • 父類別的 bar(int) 被隱藏,d.bar(10) 會編譯錯誤,必須 d.Base::bar(10) 才能呼叫。

  • 範例比較:有無 virtual 差異

    • 父類沒有 virtual → 不是覆寫

      #include <iostream>
      using namespace std;
      
      class Base {
      public:
          void show() { cout << "Base::show()\n"; }
      };
      
      class Derived : public Base {
      public:
          void show() { cout << "Derived::show()\n"; } // 只是遮蔽,不是覆寫
      };
      
      int main() {
          Base* b = new Derived();
          b->show();   // 輸出:Base::show(),因為靜態繫結
      }
      • 即使名稱與參數相同,也只是 遮蔽 (hiding),呼叫的仍是 Base::show()。沒有多態(polymorphism)效果。
    • 父類加上 virtual → 成為覆寫

      #include <iostream>
      using namespace std;
      
      class Base {
      public:
          virtual void show() { cout << "Base::show()\n"; }
      };
      
      class Derived : public Base {
      public:
          void show() override { cout << "Derived::show()\n"; } // 正確覆寫
      };
      
      int main() {
          Base* b = new Derived();
          b->show();   // 輸出:Derived::show(),因為動態繫結
      }
      • 加上 virtual 後,C++ 會在執行期依照物件真實型別(Derived)決定呼叫哪個版本,這就是 多態(polymorphism)。
    • 程式設計實務建議

      建議 原因
      若要表達「相同行為、不同實作」→ 用 override 提供多態性,讓系統可擴充
      若要表達「名稱相似但行為不同」→ 用 hiding + using 明確控制介面可見性
      避免誤遮蔽 若無 virtual 卻同名,會導致父類別函數無法呼叫
      使用 override 關鍵字 讓編譯器幫你檢查覆寫是否正確
  • 三者比較

    • Function Overloading(函數多載):在同一個可形成多載集合的作用域中,宣告相同名稱、但參數列表不同的函數,使程式可以依不同輸入呼叫相應版本。

    • Function Overriding(函數覆寫):當衍生類別提供與基底類別虛擬函數相符的函數實作時,便構成函數覆寫。

    • Function Hiding(函數遮蔽):衍生類別只要宣告了某個與基底類別成員同名的成員,在衍生類別進行名稱查找時,便可能遮蔽基底類別中所有同名函數。

    • 比較項目 Overloading 多載 Overriding 覆寫 Hiding 遮蔽
      核心目的 提供不同參數的呼叫介面 重新實作基底類別的虛擬函數 衍生類別的名稱隱藏基底類別同名成員
      是否需要繼承 不需要 需要 常見於繼承,也可發生在其他作用域
      是否需要 virtual 不需要 必須有虛擬基底函數 不需要
      函數名稱 相同 相同 相同
      參數列表 必須能構成不同多載 必須符合覆寫條件 可以相同或不同
      const 限定 可以不同 必須符合覆寫條件 可以不同
      決定行為的主要機制 Overload Resolution Virtual Dispatch Name Lookup
      與執行時期多型的關係 本身不提供 是主要實現方式 本身不提供
      常用語法 同名不同參數 virtual、override using Base::func
      典型風險 隱性轉型、呼叫歧義 簽章不符、未正確覆寫 基底類別函數意外消失於候選集合
      // Function Overloading:函數多載
      class Calculator {
      public:
        double calculate(double price) {
          return price;
        }
      
        double calculate(double price, double rate) {
          return price * (1.0 + rate);
        }
      
        double calculate(int quantity, double price) {
          return quantity * price;
        }
      };
      Calculator c;
      
      c.calculate(100.0);         // 第一個版本
      c.calculate(100.0, 0.05);   // 第二個版本
      c.calculate(10, 100.0);     // 第三個版本
      Example a;
      const Example b;
      
      a.getValue();  // 非 const 版本
      b.getValue();  // const 版本
      //Function Overriding:函數覆寫
      #include <iostream>
      
      class Instrument {
      public:
        virtual double NPV() const {
          std::cout << "Instrument::NPV()\n";
          return 0.0;
        }
      
        virtual ~Instrument() = default;
      };
      
      class Bond : public Instrument {
      public:
        double NPV() const override {
          std::cout << "Bond::NPV()\n";
          return 100.0;
        }
      };
      
      
      // 測試
      Bond bond;
      Instrument& instrument = bond;
      
      bond.NPV();
      instrument.NPV();
      // 兩次都執行:
      // Bond::NPV()
      // 
      // 原因是 Instrument::NPV() 為虛擬函數,而 Bond::NPV() 正確覆寫它。
      
      // 覆寫必要條件:
      // 條件                 說明
      // 繼承                 衍生類別必須繼承基底類別(可為間接繼承)
      // virtual            基底函數必須是虛擬函數
      // 函數名稱           必須相同
      // 參數型態列表         必須相同
      // const / volatile   必須符合覆寫條件
      // 參考限定詞    &、&&   必須符合覆寫條件
      // 回傳型態           必須相同,或符合協變回傳型態規則
      // override           非必要,但強烈建議加上
    • 最容易混淆:覆寫不是重新定義一個同名函數就好

    • class Base {
      public:
        virtual void print() const {
          std::cout << "Base\n";
        }
      };
      
      class Derived : public Base {
      public:
        void print() {
          std::cout << "Derived\n";
        }
      };
      • 注意

        virtual void print() const; // Base
        void print();               // Derived
        • 衍生類別的 print() 缺少 const。

          因此,這不是覆寫,而是遮蔽。

        • 如果衍生類別宣告為:

          void print() override;

          編譯器會直接報錯,因為沒有成功覆寫任何虛擬函數。

          這正是 override 的實務價值:讓意外的遮蔽在編譯階段就被發現。

      // Function Hiding:函數遮蔽
      #include <iostream>
      
      class Base {
      public:
        void f() {
          std::cout << "Base::f()\n";
        }
      
        void f(int x) {
          std::cout << "Base::f(int)\n";
        }
      };
      
      class Derived : public Base {
      public:
        void f(double x) {
          std::cout << "Derived::f(double)\n";
        }
      };
      Derived obj;
      
      obj.f(10);    // Derived::f(double)
      obj.f(3.14);  // Derived::f(double)
      obj.f();      // 編譯錯誤
    • 為什麼 obj.f() 發生編譯錯誤?

      因為名稱查找在 Derived 找到 f(double) 後,基底類別的 f()、f(int) 不會自動納入此查找得到的候選集合。

      注意,Base::f() 並沒有消失,也沒有被刪除。它只是被遮蔽。

      仍然可以明確呼叫:

      obj.Base::f();
      obj.Base::f(10);
// 使用 using 解除遮蔽
class Derived : public Base {
public:
  using Base::f;
  
  void f(double x) {
    std::cout << "Derived::f(double)\n";
  }
};
Derived obj;

obj.f();      // Base::f()
obj.f(10);    // Base::f(int)
obj.f(3.14);  // Derived::f(double)
  • 這裡有一個非常重要的變化:

    • 透過 using Base::f;,原本被遮蔽的基底類別函數參與多載解析,於是 Overloading 與 Hiding 之間產生了直接關係。

    • using 不是複製函數,也不會自動覆寫虛擬函數,而是把基底類別的成員宣告引入衍生類別的名稱查找範圍。

父類別指標、虛/純虛函數,多態性與解構函數

父類別指標與子類別指標

  • 物件可透過 new 創建:

    • 使用 new 運算子在『堆(heap)』上動態創建物件。動態創建物件意指物件的生存期由程式設計師顯性控制,且物件將一直存在,直到使用 delete 運算子將其銷毀。如:
    Human *phuman = new Human();
    Men *pmen = new Men;
  • 父類別的指標可以 new 一個子類別物件(但子類別的指標無法 new 一個父類別物件):

    Human *pHuman2 = new Men;
  • 故父類別指標可以指向父類別物件,也可以指向子類別物件。

  • 可透過父類別指標呼叫父類別成員函數。

  • 可透過父類別指標呼叫從父類別繼承的成员函數和子類別『覆寫(override)』的成员函數( C++ 中的『多態性』的一個重要觀念)。

  • 但無法透過父類別指標呼叫子類別『特有』的成員函數。

  • 若想呼叫子類別特有的成員函数,則需將指標或參考轉換為子類別型態。此稱為『向下轉型(downcasting)』。

    #include <iostream>
    #include <memory>
    
    // 父類別
    class Base {
    public:
        virtual ~Base() = default; // 父類別的虛解構函數
        virtual void show() {
            std::cout << "父類別的 show 函數" << std::endl;
        }
    };
    
    // 子類別
    class Derived : public Base {
    public:
        void show() override {
            std::cout << "子類別的 show 函數" << std::endl;
        }
    
        void derivedFunction() {
            std::cout << "這是子類別特有的函數。" << std::endl;
        }
    };
    
    int main() {
        // 使用智慧指標指向 Derived 物件,並將其轉換為父類別指標
        std::unique_ptr<Base> basePtr = std::make_unique<Derived>();
    
        // 調用父類別中的虛函數,實際會調用子類別的覆寫版本
        basePtr->show();
    
        // 嘗試進行向下轉型
        Derived* derivedPtr = dynamic_cast<Derived*>(basePtr.get());
    
        if (derivedPtr) {
            // 轉型成功,可以調用子類別特有的函數
            derivedPtr->derivedFunction();
        } else {
            // 轉型失敗,父類別指標並不指向 Derived 物件
            std::cout << "向下轉型失敗。" << std::endl;
        }
    
        return 0;
    }

虛函數

  • 多態性(Polymorphism):『多態性』是物件導向(Object-Oriented Programming,OOP)的核心特性之一,其允許物件可以採取多種形態。在C++中,多態性主要透過父類別指標或參考操作子類別物件來實現。這使得在不同的子類別中,可以有不同的實現方式。

  • 繼承(Inheritance):繼承是 OOP 中的一個概念,允許新創建的子類別(衍生類別)繼承現有類別(基礎類別、父類別)的特性和行為。子類別可以擴展或修改父類別的功能。

  • 虛函數和動態綁定(Virtual Functions & Dynamic Binding):

    • 在C++中,當宣告一個函數為『虛函數』時,則告訴編譯器不要在編譯時確定這個函數的調用,而是在『執行期』再作決定。

    • 在執行期的『函數調用解析』被稱為『動態綁定』或『晚期綁定(Late Binding)』。

  • 虛函數的工作方式:

    • 宣告虛函數:在父類別中,透過在函數宣告前加上關鍵字 virtual 來創建『虛函數』。

    • 覆蓋虛函數:在子類別中,可以覆蓋這個函數。這稱為『函數覆寫(Function Overriding)』。注意:不是函數重載(Function Overloading)。

    • 動態綁定:當透過父類別的指標或參考呼叫虛函數時,注意,虛函數的調用不是在編譯時解析的,而是 C++在執行時會檢查這個指標或參考實際指向或綁定的物件型態,並呼叫該物件型態的函數版本。

    • C++ 使用『虛函數表(vtable)』來實現『動態綁定』。

    • 虛函數表(vtable):每個使用虛函數的類別都有一個『虛函數表』。這是一個『函數指標』的『陣列』,指向該類別中所有虛函數的實現。當類別的『物件』被創建時,vtable 也會跟著被創建。

    • 虛指針(vptr):每個物件都有一個隱藏的指標(稱為虛指標,或 vptr),指向該物件類別的 vtable。當透過父類別指標或參考呼叫虛函數時,會使用這個 vptr 來確定實際該呼叫哪個函數。

    class Animal {
    public:
        virtual void speak() {
            cout << "Some animal sound" << endl;
        }
    };
    
    class Dog : public Animal {
    public:
        void speak() override {
            cout << "Bark" << endl;
        }
    };
    
    class Cat : public Animal {
    public:
        void speak() override {
            cout << "Meow" << endl;
        }
    };
    • 當創建一個 Animal 指標並讓它指向 Dog 或 Cat 的物件時,呼叫 speak 函數會根據物件的實際型態(Dog 或 Cat)來決定調用哪個 speak 的實現。
    Animal* a = new Dog();
    a->speak();  // 輸出 "Bark"
    
    Animal* b = new Cat();
    b->speak();  // 輸出 "Meow"
  • 純虛函數:如果虛函數在父類別中沒有被實作,並且期望在子類別中被實作出來,則稱為『純虛函數』。純虛函數是用 = 0 語法宣告的。

    virtual void pureVirtualFunction() = 0;
  • 『抽象類別』是一種包含一個或多個『純虛函數』的類別。這些純虛函數在抽象類別中沒有實作,只提供了『介面』,並要求子類別必須實作出這些函數。

  • 抽象類別不能直接『實例化』,只能作為基礎類別,並透過子類別來實現具體功能。

    • QuantLib::Instrument 是一個『抽象類別』,包含多個『純虛函數』。以下是 Instrument 類別的一部分定義:

      namespace QuantLib {
      
          class Instrument {
          public:
              virtual bool isExpired() const = 0;
              virtual void setupArguments(PricingEngine::arguments*) const = 0;
              virtual void fetchResults(const PricingEngine::results*) const = 0;
              virtual void setupExpired() const = 0;
      
              virtual double NPV() const = 0; // 純虛函數,用於計算淨現值
          };
      
      }
    • 要使用 QuantLib::Instrument,我們需要定義一個子類別並實作所有純虛函數。

      // MyBond.hpp
      #include <ql/instrument.hpp>
      
      class MyBond : public QuantLib::Instrument {
      public:
          MyBond(double faceValue, double couponRate, int yearsToMaturity);
      
          bool isExpired() const override;
          void setupArguments(QuantLib::PricingEngine::arguments* args) const override;
          void fetchResults(const QuantLib::PricingEngine::results* results) const override;
          void setupExpired() const override;
      
          double NPV() const override;
      
      private:
          double faceValue_;
          double couponRate_;
          int yearsToMaturity_;
      };
      // MyBond.cpp
      #include "MyBond.hpp"
      
      MyBond::MyBond(double faceValue, double couponRate, int yearsToMaturity)
          : faceValue_(faceValue), couponRate_(couponRate), yearsToMaturity_(yearsToMaturity) {}
      
      bool MyBond::isExpired() const {
          // 假設債券到期日期已過
          return yearsToMaturity_ <= 0;
      }
      
      void MyBond::setupArguments(QuantLib::PricingEngine::arguments* args) const {
          // 設定評價引擎所需的參數
      }
      
      void MyBond::fetchResults(const QuantLib::PricingEngine::results* results) const {
          // 從評價引擎得到結果
      }
      
      void MyBond::setupExpired() const {
          // 設置到期時的行為
      }
      
      double MyBond::NPV() const {
          // 計算債券的淨現值
          double npv = 0.0;
          for (int i = 1; i <= yearsToMaturity_; ++i) {
              npv += (faceValue_ * couponRate_) / std::pow(1.05, i); // 假設折現率為5%
          }
          npv += faceValue_ / std::pow(1.05, yearsToMaturity_);
          return npv;
      }
  • 抽象類別的功能:

    • 設計靈活的架構:抽象類別提供了一個介面,使得具體實現可以獨立於抽象介面進行變化,從而實現靈活且可擴展的架構。

    • 強制實現特定行為:派生類別必須實現所有純虛函數,這保證了所有派生類別都具備抽象類別所定義的基本行為。

  • 虛函數覆寫(overriding):在子類別中重新定義父類別的虛函數稱為覆寫。C++11 引入了 override 關鍵字來明確指示函數覆寫了父類別的虛函數。

    • 使用 override 關鍵字不是強制性的,其被用來覆寫父類別的虛函數。

    • 在 C++ 中,即使不使用 override 關鍵字,子類別也可以覆寫父類別的虛函數,只要子類別中的函數具有與父類別中的虛函數相同的簽名(即相同的函數名、回傳類型和參數列表)。

    • 但 override 關鍵字在現代 C++ 程式設計中是非常有用的,因為它提供了額外的『型態檢查』。

    • 當在子類別中的函數宣告後使用 override 關鍵字時,如果該函數無法有效覆寫父類別中的任何虛函數(例如,由於簽名不匹配),編譯器將拋出錯誤。這有助於在開發過程中及早發現錯誤,從而減少了誤解和潛在的錯誤。

      class Base {
      public:
          virtual void someFunction();
      };
      
      class Derived : public Base {
      public:
          void someFunction() override; // 正確地覆蓋基礎類別的虛函數
          void anotherFunction() override; // 錯誤:Base 沒有這個虛函數
      };
      • 在這個例子中,如果 Base 類別沒有名為 anotherFunction 的虛函數,則在編譯時 Derived 類的 anotherFunction 會因使用了 override 關鍵字而產生編譯錯誤。

      • 故 override 關鍵字雖然為可選的,但建議使用,因其可增加程式碼的清晰度和安全性。

虛解構函數

  • 在 C++ 中,如果一個類別被設計為父類別並且其解構函數 『不是』 虛函數(virtual),則當透過父類別指標刪除子類別物件時,子類別的解構函數不會被調用,進而導致資源洩漏。

  • 這是因為非虛解構函數僅根據指標的型別(而非物件的實際類型)來決定執行哪個解構函數。

  • 問題說明(非虛解構函數導致的資源洩漏):

#include <iostream>
using namespace std;

class Base {
public:
    ~Base() {
        cout << "Base destructor" << endl;
    }
};

class Derived : public Base {
private:
    int* data;
public:
    Derived() {
        data = new int[10];  // 分配動態記憶體
        cout << "Derived constructor: Memory allocated" << endl;
    }

    ~Derived() {
        delete[] data;  // 釋放動態記憶體
        cout << "Derived destructor: Memory deallocated" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // 只調用 Base 的解構函數
    return 0;
}


// Base destructor
  • 改良版:

    • 任何作為父類別的類別都應有虛解構函數:

      • 如果類別設計為父類別,或者可能被繼承,應將解構函數聲明為虛函數。
    • 純虛解構函數:

      • 如果父類別不需要實例化,可以將解構函數定義為『純虛(= 0)』,但仍需提供其實現。
    • 先呼叫子類別的解構函數,再呼叫父類別的解構函數。

#include <iostream>
using namespace std;

class Base {
public:
    virtual ~Base() = 0;  // 純虛解構函數
};

Base::~Base() {
    cout << "Base destructor" << endl;
}

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived destructor" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // 正確調用所有解構函數
    return 0;
}

QuantLib 中的「函數覆寫 (Override)」運用

  • 在 QuantLib 內,幾乎所有金融工具的評價、現金流生成、殖利率曲線計算 都透過「虛函數覆寫」實作多態。

  • 父類別通常是抽象基底類 (abstract base class),定義一組統一的介面(例如 Bond, Instrument, PricingEngine, TermStructure),而子類別根據不同模型或產品「覆寫」這些虛函數以提供具體實作。

  • Instrument → Bond → FixedRateBond (檔案:ql/instrument.hpp, ql/instruments/bond.hpp)

    // Instrument.hpp
    class Instrument : public Observable, public Observer {
      public:
        virtual ~Instrument() = default;
    
        // 虛函數:所有金融工具都要實作自己的淨現值計算方式
        virtual void setupArguments(PricingEngine::arguments*) const = 0;
        virtual void fetchResults(const PricingEngine::results*) const = 0;
        virtual bool isExpired() const = 0;
    
        Real NPV() const { return NPV_; }  // 非虛,通用介面
      protected:
        mutable Real NPV_;
    };
    // Bond.hpp
    class Bond : public Instrument {
      public:
        void setupArguments(PricingEngine::arguments* args) const override;
        bool isExpired() const override;
    };
    // FixedRateBond.hpp
    class FixedRateBond : public Bond {
      public:
        void setupArguments(PricingEngine::arguments* args) const override;
    };
    • Instrument 定義了『抽象介面』 setupArguments()、fetchResults()、isExpired()。

    • Bond 覆寫 isExpired(),提供通用債券邏輯。

    • FixedRateBond 再覆寫 setupArguments(),加入固定利率債的特有欄位。

    • 這種架構讓 QuantLib 的引擎 (BondEngine, DiscountingBondEngine, BlackBondEngine)可以用同樣的呼叫方式 (bond->NPV()) 來對不同債券類別運作。

  • YieldTermStructure → FlatForward, ZeroCurve, InterpolatedDiscountCurve (檔案:ql/termstructures/yieldtermstructure.hpp)

    class YieldTermStructure : public TermStructure {
      public:
        virtual Rate zeroRate(const Date& d, const DayCounter&, Compounding, Frequency, bool extrapolate = false) const;
        virtual DiscountFactor discount(const Date& d, bool extrapolate = false) const = 0;
    };
    // ql/termstructures/yield/flatforward.hpp
    class FlatForward : public YieldTermStructure {
      public:
        DiscountFactor discount(const Date& d, bool extrapolate = false) const override;
    };
    • YieldTermStructure 提供抽象的「折現曲線」介面。

    • 子類別 FlatForward 覆寫 discount(),使用固定遠期利率計算。

    • 這樣在評價器中只要持有 shared_ptr<YieldTermStructure>,就能動態使用不同曲線模型,達成『多態性』。

    class PricingEngine : public Observable {
      public:
        struct results { virtual ~results() = default; };
        struct arguments { virtual ~arguments() = default; };
        virtual void calculate() const = 0;
    };
    
    // 子類別
    class DiscountingBondEngine : public PricingEngine {
      public:
        void calculate() const override;
    };
    • 所有評價引擎都透過 calculate() 虛函數統一呼叫。

    • 根據實際傳入的引擎(Black-Scholes、Hull-White、Discounting…),會自動切換邏輯。

    • QuantLib 使用這種覆寫架構讓新產品/模型能直接 plug-in。

  • EX:策略模型:把「演算法」抽離成可以替換的物件,在執行時動態切換

    • 定義一個 抽象的定價引擎 PricingEngine。

    • 實作一個 具體的 DiscountingBondEngine,用折現法算債券價格。

    • 定義一個 Bond 類別,裡面不自己算價格,而是呼叫外面塞進來的引擎。

    • main() 裡組一個 3 年期債券,指定折現率,算出 NPV。

    • calculate() 是唯一的策略接口(Strategy Interface)

    • 純虛 → 強迫所有策略都要實作自己的演算法

    • 每一個引擎都是一個獨立演算法模組

    • QuantLib 整個架構就是靠 Strategy Pattern 撐起來的。

    #include <iostream>
    #include <vector>
    #include <memory>
    #include <cmath>
    
    // Strategy Pattern(策略模式):把「演算法」抽離成可以替換的物件,在執行時動態切換。
    // Bond 不自己算價格,而是把計算外包給「可以更換的引擎」。
    // Bond 都不用改,但可以任意切換引擎。
    // 之後可以換成:
    //            DurationBondEngine
    //            MonteCarloBondEngine
    //            CurveFittingBondEngine
    //            MonteCarloBondEngine
    
    // ================================
    // 1. Observable 基類(簡化版)
    // 目前這個 Observable 裡 什麼都沒有,只有一個虛擬 destructor。
    // 在「正式的 QuantLib」裡,Observable 會負責維護 
    // observer清單、通知變動。但這邊只保留接口,讓你看到「引擎是可以被觀察的」這個概念。
    // ================================
    class Observable
    {
      public:
        virtual ~Observable() = default;
    };
    
    // ================================
    // 2. PricingEngine(抽象引擎)抽象基底類別
    // ================================
    // PricingEngine 繼承 Observable → 表示它是「可以被觀察的物件」。
    // 在完整版本裡,Instrument 會註冊成 observer,一旦市場資料改變,engine 或 curve 改變,就會通知。
    class PricingEngine : public Observable
    {
      public:
        // 這兩個是 引擎的輸入 / 輸出資料結構的「接口」。
        // 基底的 arguments / results 裡面沒有欄位,只提供虛擬 destructor。
        // 用意是:
        //       各種不同產品的引擎,都可以 繼承 這兩個 struct,自訂欄位:
        //         債券:到期日、票面金額、現金流陣列、貼現率 …
        //         選擇權:標的價格、波動度、strike、到期日 …
        // 因為 destructor 是 virtual,以後可以用 base pointer 安全 delete。
        struct arguments
        {
            virtual ~arguments() = default;
        };
    
        struct results
        {
            virtual ~results() = default;
        };
    
        // 純虛函式
        // 這一行讓 PricingEngine 變成 抽象類別:
        //         不能 new PricingEngine。
        //         一定要由 子類別 override 這個函式。
        // 之後所有具體的引擎(例如 DiscountingBondEngine)都會實作 calculate(),在裡面用 arguments 算出 results。
        virtual void calculate() const = 0;
    
        // 成員指標
        // 為了支援多型:真實使用時,這兩個指標會指向 衍生類別,例如:
        //       DiscountingBondEngine::arguments
        //       DiscountingBondEngine::results
    
        // mutable: 需要在 calculate() 裡 讀寫 arguments_ / results_ 指向的東西。
        mutable arguments* arguments_ = nullptr;
        mutable results*   results_   = nullptr;
    };
    
    // ================================
    // 3. CashFlow 結構
    // ================================
    struct CashFlow
    {
        double amount;
        double time; // 以「年」為單位的時間
    
        CashFlow(double amt, double t) : amount(amt), time(t) {}
    };
    
    // ================================
    // 4. DiscountingBondEngine(子引擎)
    // 表示這個類別是一個 具體的定價引擎,可拿來算價格。
    // 它要滿足:
    //       實作 calculate() const
    //       定義自己專用的 arguments 和 results(繼承自基底)。
    // ================================
    class DiscountingBondEngine : public PricingEngine
    {
      public:
        // 這個 struct 是 專門給這個引擎使用的輸入格式。
        //   cashflows:所有的現金流(每一年利息、本金)。
        //   discountRate:貼現率 r(假設是固定年利率,不是整條曲線)。
        // 在 QuantLib 裡,這通常會放:std::vector<boost::shared_ptr<CashFlow>>、Handle<YieldTermStructure> discountCurve; 等。
        struct arguments : PricingEngine::arguments
        {
            std::vector<CashFlow> cashflows;
            double                discountRate; // 固定年利率 r
        };
    
        // 把計算結果(債券價格)放在 npv 欄位。 
        struct results : PricingEngine::results
        {
            double npv = 0.0;
        };
    
        void calculate() const override
        {   
            // arguments_ 型別是 PricingEngine::arguments*,但實際上指向的是 DiscountingBondEngine::arguments。
            // 用 dynamic_cast 轉回真實型別,方便存取欄位 cashflows、discountRate。
            // results_ 同理,轉回 DiscountingBondEngine::results,才能寫 res->npv。
            auto* args = dynamic_cast<const arguments*>(arguments_);
            auto* res  = dynamic_cast<results*>(results_);
    
            // 折現公式
            double total = 0.0;
            for (const auto& cf : args->cashflows)
            {
                double df = std::exp(-args->discountRate * cf.time);
                total += cf.amount * df;
            }
    
            res->npv = total;
        }
    };
    
    // ================================
    // 5. Bond instrument
    //    Bond 類別:Instrument,負責持有現金流與引擎
    //               放現金流
    //               將 arguments 傳給引擎
    //               呼叫 engine_->calculate()
    // ================================
    class Bond
    {
      public:
        // 傳入一組現金流,move 進 cashflows_,避免多餘複製。
        // cashflows_ 是這支債券的所有現金流資訊。
        Bond(std::vector<CashFlow> cfs) : cashflows_(std::move(cfs)) {}
    
        // Bond 本身不管引擎是什麼,只知道「要有一個 PricingEngine」。
        // 這實際上就是一種 策略模式(Strategy Pattern):
        //       「債券」這個 context,把計算邏輯委託給外部注入的 strategy(引擎)。
        void setPricingEngine(std::shared_ptr<PricingEngine> engine)
        {
            engine_ = engine;
        }
    
        double NPV(double discountRate) const
        {
            // 建立引擎用的輸入與輸出物件
            auto* args = new DiscountingBondEngine::arguments();
            auto* res  = new DiscountingBondEngine::results();
    
            // 把債券資料填進 arguments
            args->cashflows    = cashflows_;
            args->discountRate = discountRate;
    
            // 將 arguments / results 交給 engine
            engine_->arguments_ = args;
            engine_->results_   = res;
    
            // 呼叫引擎計算
            engine_->calculate();
            double value = res->npv;
    
            // 釋放記憶體 & 回傳結果
            delete args;
            delete res;
    
            return value;
        }
    
      private:
        // cashflows_:債券內部的現金流。
        // engine_:指向一個外部提供的定價引擎。用 shared_ptr 是為了在多個 instrument 之間共享同一個引擎時比較方便。
        std::vector<CashFlow>          cashflows_;
        std::shared_ptr<PricingEngine> engine_;
    };
    
    // ================================
    // 6. 主程式
    // ================================
    int main()
    {
        std::cout << "=== Bond Pricing Engine Example ===\n";
    
        // 建立 3 年期固定利率債券
        // 每年支付 5 元,最後返還本金 100 元
        std::vector<CashFlow> cfs = {
            CashFlow(5.0, 1.0), CashFlow(5.0, 2.0), CashFlow(105.0, 3.0) // 100 本金 + 5 利息
        };
    
        Bond bond(cfs);
    
        // 使用折現法引擎
        auto engine = std::make_shared<DiscountingBondEngine>();
        bond.setPricingEngine(engine);
    
        double r = 0.03; // 3% discount rate
    
        double price = bond.NPV(r);
    
        std::cout << "Discount rate = " << r << "\n";
        std::cout << "Bond price    = " << price << "\n";
    
        return 0;
    }
  • Bond 是不會算價格的;

    • 是 PricingEngine 負責算價格;

    • 而 PricingEngine 可以自由替換不同的演算法。

    • 這就是 Strategy Pattern。

QuantLib 中的「函數遮蔽 (Function Hiding)」運用

  • 相較於 override,QuantLib 使用 hiding 的情況較少。

  • 主要是為了「避免父類別虛函數干擾」或「提供重載版本」。

  • Date 類別中重載與遮蔽 (ql/time/date.hpp)

    class Date {
      public:
        // 重載版本(不是虛函數)→ 根據輸入類型遮蔽其他同名版本
        static Date minDate();
        static Date maxDate();
    
        Integer dayOfMonth() const;
        Integer month() const;
        Integer year() const;
    
        // 隱藏掉其他同名重載的 toString()
        std::string toString() const;
    };
    • toString() 並沒有覆寫任何東西,只是「遮蔽」了 std:: 的同名函數。

    • QuantLib 明確希望自己的 Date::toString() 不受 std::to_string() 影響。

  • Observer / Observable 的重載遮蔽 (ql/patterns/observable.hpp)

    class Observer {
      public:
        virtual void update() = 0;
    };
    
    class Observable {
      public:
        void notifyObservers() const;
        void notifyObservers(const void* tag) const; // 同名不同參數 → 函數遮蔽
    };
    • notifyObservers() 有兩個重載版本。

    • 它們不是虛函數,因此不是覆寫,而是同名遮蔽(overload hiding)。

    • 這裡的設計用意是提供「一般通知」與「帶識別碼通知」兩種版本。

  • QuantLib 透過 override 建立「金融產品與模型的多態架構」,而 hiding 僅用於「重載介面設計」與「避免父類別干擾」。

概念 發生範圍 是否需要 virtual 決定時機 多態行為 QuantLib 是否常用
Overloading (重載) 同一類別內 否 編譯期 無 常見
Override (覆寫) 父類別 ↔︎ 子類別 要有 virtual 執行期 有 更常見
Hiding (遮蔽) 父類別 ↔︎ 子類別 否 編譯期 無 偶爾出現

友函數(friend Function)

  • C++ 中的友函數(Friend Function)是一種特殊的函數,它被允許存取一個類別的 private 和 protected 成員。

  • 友函數不是類別的成員函數,但它被指定為該類別的『朋友(Friend)』。

  • 宣告 friend 並不代表「這個函數屬於類別」;

    • 它仍然是個「普通的全域函數」,但獲得了「特許通行證」──可以存取該類別的私有資料。
  • 把這個函數列入「可以存取我的 private/protected」的白名單。

  • 不改變函數的作用域(仍是全域函數)。

  • 特點:

    • 存取權限:友函數可以存取該類的『所有成員』,包括private 和 protected成員。

    • 非成員函數:友函數不是類別的成員函數,因此它不是由物件來呼叫的。它就像一般的函數,但有存取特定類別的private和protected成員的能力。

    • 宣告方式:在『類別的定義內』使用 friend 關鍵字來宣告友函數。

    • 互不影響:友函數的宣告不影響類別的封裝特性。類別的實現細節仍然隱藏於使用者。

    • 作用範圍:友函數的作用範圍不是類別的範圍,而是與它被定義的區域相同。

  • 友函數可以是全局函數,也可以是另一個類別的成員函數。

友函數通常用於當函數需要存取多個類別的私有成員時。例如,重載某些運算子(如 << 或 >> 用於輸出/輸入)時,可能需要存取類別的私有成員變數。

class MyClass {
private:
    int x;

public:
    MyClass(int val) : x(val) {}

    // 宣告友函數
    friend void showX(MyClass&);
};

// 定義友函數
void showX(MyClass& m) {
    // 直接存取私有成員 x
    std::cout << "MyClass::x = " << m.x << std::endl;
}

int main() {
    MyClass obj(10);
    showX(obj); // 輸出 MyClass::x = 10
    return 0;
}
class MyClass {
private:
    int value;

public:
    MyClass(int v) : value(v) {}

    // 友函數
    friend void showValue(MyClass&);
};

// 全局範圍內定義的友函數
void showValue(MyClass& m) {
    std::cout << "Value: " << m.value << std::endl;
}

int main() {
    MyClass obj(100);
    showValue(obj);  // 在全局範圍內調用
    return 0;
}
  • 在這個例子中,showValue 是一個全局函數,它被宣告為 MyClass 的友函數,因此能夠存取 MyClass 的私有成員 value。儘管 showValue 被宣告為友函數,但它仍然是一個全局函數,可以在全局範圍內被調用。
class Point {
    int x_, y_;
public:
    Point(int x, int y): x_(x), y_(y) {}
    friend std::ostream& operator<<(std::ostream&, const Point&);
};

std::ostream& operator<<(std::ostream& os, const Point& p) {
    return os << "(" << p.x_ << "," << p.y_ << ")"; //  OK
}
// 非成員(友函數)版本
class Vector {
    double x_, y_;
public:
    Vector(double x, double y) : x_(x), y_(y) {}

    // 非成員函數
    friend Vector operator+(const Vector& lhs, const Vector& rhs) {
        return Vector(lhs.x_ + rhs.x_, lhs.y_ + rhs.y_);
    }
};

int main() {
    Vector a(1, 2), b(3, 4);
    Vector c = a + b;   // 等同於 operator+(a, b)
}
// 成員函數版本
class Vector {
    double x_, y_;
public:
    Vector(double x, double y) : x_(x), y_(y) {}

    // 成員函數版本:左邊是自己
    Vector operator+(const Vector& rhs) const {
        return Vector(x_ + rhs.x_, y_ + rhs.y_);
    }
};

int main() {
    Vector a(1, 2), b(3, 4);
    Vector c = a + b;   // 等同於 a.operator+(b)
    // Vector c = a + 3;   // 不行
    // Vector d = 3 + a;   // 更不行
}

補充觀念:

  • 所有函數(成員或非成員)都在程式的「code segment」中,不會放進物件的資料記憶體。

  • 成員函數之所以能「看到物件的私有成員」,是因為它被編譯器自動加上了 this 指標(指向該物件的記憶體)。

  • friend 函數仍然是「普通函數」,沒有 this,但在編譯階段被授權「可直接存取」該類別的私有區。

  • 因此從記憶體角度看:

    • friend 和一般非成員函數的機械碼、呼叫機制完全相同。

    • 唯一差別是「編譯器允許它看 private 成員」。

  • friend 並不會改變「函數放在哪裡」或「記憶體結構」,它只是在編譯階段告訴編譯器:「這個函數有權看我的內部資料」。

    • 在執行時(runtime),friend 函數與一般函數完全一樣。

    • 在編譯時(compile-time),friend 函數多了一個「通行證」。

  • friend 關係不具繼承性:在 C++ 的繼承模型中,會繼承「成員」(函數、資料)但不會繼承「授權關係」。

    class A {
        int x_ = 10;
        friend void inspect(const A&, const B&);
    };
    class B {
        int y_ = 20;
        friend void inspect(const A&, const B&);
    };
    
    void inspect(const A& a, const B& b) {
        std::cout << a.x_ << ", " << b.y_ << std::endl;
    }
    • 這裡 inspect() 同時被兩個類別授權,可以同時讀取 A::x_ 和 B::y_。

    • 但這完全不影響記憶體結構。inspect() 仍然是一個普通全域函數,只是被兩個類別「白名單授權」。

友類別(friend class)

  • 友類別(Friend Class)是一種允許某個類別存取另一個類別的私有成員和保護成員的機制。

  • QuantLib 中 Handle<T> 可以存取某些曲線類的內部資料。

  • friend 只是「編譯期」的權限設定。

  • 告訴編譯器:B 類別可以存取 A 類別的 private / protected 成員。

    • 所以記憶體上:

      • A 物件仍然是一塊固定大小的記憶體

      • B 物件也是自己的記憶體

      • 兩者 沒有額外的指標或參考

      • 沒有多做任何 new/delete

    • 它是純語法層級的授權,不影響執行期記憶體。

  • friend 是強大的武器,但濫用會破壞封裝性。

  • 通常,私有成員只能在類別的成員函數內部存取,而友類別可以越過這些限制,直接存取其他類別的私有成員和保護成員。

    • 存取私有和保護成員:友類別可以存取另一類別的私有和保護成員,這使得在某些情況下,設計上更方便。
    • 友類別並非繼承關係:被指定為友類別的類並不會繼承另一個類的私有成員,它只是一種特殊的授權機制。
    • 指定為友類別:可以在類別的內部使用 friend 關鍵字來指定哪些類是該類的友類別。
    class B;  // 前向宣告
    
    class A {
        private:
            int x;
        public:
            A() : x(10) {}  // 建構子
            friend class B;  // B 是 A 的友類別
    };
    
    class B {
        public:
            void printA(const A& a) {
                // B 類可以存取 A 類的私有成員
                std::cout << "x = " << a.x << std::endl;
            }
    };
    
    int main() {
        A a;
        B b;
        b.printA(a);  // 可以存取 A 類的私有成員
        return 0;
    }
    • 友類別的定義:在 A 類中,我們使用 friend class B; 來將 B 類標註為 A 類的友類別。
    • 友類別的權限:在 B 類的 printA 函數中,我們可以直接存取 A 類的私有成員 x。這是因為 B 是 A 的友類別,即使 x 是私有成員,B 仍然可以存取它。
    • 友類別的作用範圍:友類別只能存取該類別的私有成員和保護成員,但無法繼承或改變它們的存取控制。
    class A {
        friend class B;      // B 是 A 的朋友
    private:
        int secret_ = 42;
    };
    
    class B {
    public:
        void showSecret(const A& a) {
            std::cout << a.secret_ << std::endl;  // ✅ 可以存取 private 成員
        }
    };
    
    int main() {
        A a;
        B b;
        b.showSecret(a);  // 輸出 42
    }
  • B 是「A 的朋友」;

  • 所以 B 裡的任何成員函數都可以看到 A 的內部資料;

  • 但反過來不行(A 看不到 B 的 private)。

    #include <iostream>
    #include <string>
    
    class Manager;  // 前向宣告
    
    class Employee {
        private:
            std::string name;
            double salary;
    
        public:
            Employee(const std::string& n, double s) : name(n), salary(s) {}
    
            // 宣告 Manager 為友類別
            friend class Manager;
    
            void displayInfo() const {
                std::cout << "Employee: " << name << ", Salary: " << salary << std::endl;
            }
    };
    
    class Manager {
        public:
            void giveRaise(Employee& e, double amount) {
                // 由於 Manager 是 Employee 的友類別,所以可以直接修改 Employee 的私有成員
                e.salary += amount;
                std::cout << "Salary after raise: " << e.salary << std::endl;
            }
    
            void displayEmployeeInfo(const Employee& e) {
                // 也可以存取 Employee 的私有成員
                std::cout << "Manager viewing: " << e.name << "'s salary: " << e.salary << std::endl;
            }
    };
    
    int main() {
        Employee emp1("John", 50000);
        Manager mgr;
    
        // 顯示員工資訊
        emp1.displayInfo();
    
        // 管理員查看員工的私有成員
        mgr.displayEmployeeInfo(emp1);
    
        // 管理員給員工加薪
        mgr.giveRaise(emp1, 5000);
    
        return 0;
    }
    • Employee 類:這個類有兩個私有成員,name 和 salary,以及一個公共方法 displayInfo() 用來顯示員工的基本信息。
    • Manager 類:這個類有兩個方法:
      • giveRaise():該方法可以給員工加薪,這是因為 Manager 是 Employee 類的友類別,可以直接存取員工的私有成員 salary。

      • displayEmployeeInfo():這個方法允許 Manager 查看員工的私有資訊(如薪水)。

    • 友類別關係:在 Employee 類中,使用 friend class Manager; 將 Manager 類設為友類別。這樣,Manager 類的成員函數就能夠存取 Employee 類的私有成員。
    class Product {
        friend class ProductBuilder;   // 授權 builder
    private:
        std::string name_;
        double price_ = 0.0;
    };
    
    class ProductBuilder {
        Product p_;
    public:
        ProductBuilder& setName(std::string n) { p_.name_ = std::move(n); return *this; }
        ProductBuilder& setPrice(double pr) { p_.price_ = pr; return *this; }
        Product build() { return p_; }
    };
    • ProductBuilder 可以直接設定 Product 的 private 成員,但外部仍然無法改動,確保封裝性。
  • friend class 是「整體授權版本的 friend 函數」。

  • 它不會改變記憶體結構,不會讓兩個物件共享空間,只是在編譯階段讓編譯器放行:「B 可以看 A 的內部」。

  • 在 QuantLib 裡,「友類別(friend class)」的應用非常典型,最著名的例子有 Handle<T> 與 RelinkableHandle<T>。另外,QuantLib 利用 friend 讓 Handle/Observer 可以操作 observers_

    • 這組類別的設計非常優雅,展示了「友類別」如何在不破壞封裝的前提下允許另一類直接操作內部狀態。

    • Handle<T> 需要讀寫 被觀察物件(Observable) 的私有狀態

    • Observable 也需要管理 誰正在觀察它(observer list)

    • Handle<YieldTermStructure> 會直接操作曲線的私有狀態)

    • TermStructure 的 bootstrapper 需要直接存取曲線的 private 欄位(例如 discount vector、nodes、dates)

    • Instrument 把 arguments & results 託付給 Engine,使用 friend 授權 Engine 寫入。

多重繼承(multiple inheritance)

  • 多重繼承是指一個衍生類別(Derived Class)可以同時繼承兩個或多個基底類別(Base Class)。

  • C++ 允許衍生類別透過多重繼承,組合不同基底類別所提供的資料成員、成員函數及行為介面。

  • 多重繼承可以提高程式設計的彈性,但也可能引發以下問題:

    • 名稱衝突(Name Ambiguity):不同基底類別具有相同名稱的成員,造成編譯器無法判斷應使用哪個成員。
    • 菱形繼承(Diamond Inheritance):多個繼承路徑通往同一個共同基底類別,可能產生重複的基底類別子物件。

    • 建構與解構順序(Construction and Destruction Order):多個基底類別的初始化及解構有其固定順序。

    • 設計複雜度(Design Complexity):繼承關係過於複雜,可能增加程式維護及理解的困難。

  • C++ 可以使用虛擬繼承(Virtual Inheritance),避免菱形繼承產生重複的共同基底類別子物件。

  • C++ 與 Python 均支援類別的多重繼承;Java 則不允許一個類別同時繼承多個類別,但允許實作多個介面(Interfaces)。

  • 語法:

    class Derived : public Base1, public Base2 {
        // ...
    };
  • 在多重繼承中,父類別的構造函數執行順序與繼承列表的定義順序一致,而與子類別構造函數中的初始化列表順序無關。

    #include <iostream>
    using namespace std;
    
    class A {
    public:
        A() { cout << "Constructing A" << endl; }
    };
    
    class B {
    public:
        B() { cout << "Constructing B" << endl; }
    };
    
    class C : public A, public B {
    public:
        C() { cout << "Constructing C" << endl; }
    };
    
    int main() {
        C obj;
        return 0;
    }
    • C 的繼承列表是 public A, public B,因此構造順序為 A -> B -> C。

    • 如果繼承順序改為 public B, public A,輸出則為

      Constructing B
      Constructing A
      Constructing C
  • 成員存取順序:

    • 如果父類別有同名成員,子類別在多重繼承中需要明確指定要存取哪個父類別的成員。
    • 先繼承的基類優先出現在派生類的名稱查找範圍中。
    • C++ 對多重繼承物件的記憶體佈局是由編譯器決定的,父類別的繼承順序可能會影響記憶體分配和存取效率。
  • 問題一:名稱衝突(Name Ambiguity)

    class A {
    public:
      void display() {
        std::cout << "A::display()\n";
      }
    };
    
    class B {
    public:
      void display() {
        std::cout << "B::display()\n";
      }
    };
    
    class C : public A, public B {
    };
    
    
    // 因為 A 與 B 都存在 display(),編譯器無法確定應選擇哪一個基底類別的成員。
    
    C obj;
    
    // obj.display();  // 編譯錯誤:名稱查找具有歧義
    
    obj.A::display();
    obj.B::display();
    
    // 這類名稱衝突不需要菱形繼承就會發生。 只要兩個獨立基底類別提供相同名稱,就可能引發歧義。
  • 問題二:菱形繼承:

    • 多重繼承可能引發菱形繼承问题:意指子類別繼承了多個類別,而該類別最终都繼承自同一個基礎類別。這可能導致二義性,因為子類別不知道應該使用哪個基礎類別的成员。

      • 父類別的多次建構:最終子類別會多次建構同一個父類別的物件。

      • 成員或方法的二義性:子類別無法區分應該呼叫哪一個父類別中的方法或成員。

    • class A {
      public:
          void show() {
              std::cout << "Class A" << std::endl;
          }
      };
      
      class B : public A {
      public:
          void show() {
              std::cout << "Class B" << std::endl;
          }
      };
      
      class C : public A {
      public:
          void show() {
              std::cout << "Class C" << std::endl;
          }
      };
      
      class D : public B, public C {
      };
      
      
      
      int main() {
          D d;
          // d.show(); // 這行會導致編譯錯誤,因為編譯器不知道應該調用 B::show() 還是 C::show()
          return 0;
      }
      A
      | \
      B  C
      \ /
       D
      • 當一個子類別D繼承了兩個或更多的父類別,而這些父類別都繼承自同一個基礎類別 A 時,可能會在 D類別中存取基礎類A的成員時引發『二義性』問題。這是因為編譯器無法確定應該使用哪個父類別(B或C)的A成員,因此導致『二義性』。

虛擬繼承

  • C++中用於解決多重繼承二義性問題的概念。

  • 當一個類別從多個父類別繼承,而這些父類別之間有共同的基礎類別時,可能會導致二義性。

  • C++ 提供 virtual 繼承,讓多個繼承路徑共享相同的虛擬基底類別子物件。

  • 虛擬繼承允許在繼承鏈中的某個地方將基礎類別的繼承標記為『虛擬』,從而解決這種二義性。

  • 當一個類別使用虛擬繼承從一個基礎類別繼承時,它只會擁有一個該基礎類別的子物件,而不論它從多個路徑繼承該基處類別。這樣可確保在多重繼承情況下,基礎類別的成員不會產生二義性。

  • 虛擬繼承是由在最後的子類別控制虛擬父類別的建構。

  • 使用虛擬繼承時,可以在基礎類別的繼承前加上 virtual 關鍵字,例如:

    #include <iostream>
    
    class Base {
    public:
        int data;
    };
    
    class VirtualDerived1 : virtual public Base {
        // 其他成員和函數
    };
    
    class VirtualDerived2 : virtual public Base {
        // 其他成員和函數
    };
    
    class DiamondDerived : public VirtualDerived1, public VirtualDerived2 {
        // 其他成員和函數
    };
    
    int main() {
        DiamondDerived d;
        d.data = 42; // 沒有二義性,因為只有一個 Base 的實例
        std::cout << "d.data = " << d.data << std::endl;
        return 0;
    }
    • 考慮以下情境:

      1. VirtualDerived1 繼承自 Base。

      2. VirtualDerived2 繼承自 Base。

      3. DiamondDerived 同時繼承自 VirtualDerived1 和 VirtualDerived2。

      • 若不使用虛擬繼承,則 DiamondDerived 會在繼承過程中擁有兩個 Base 子物件,一個來自 VirtualDerived1,一個來自 VirtualDerived2。這可能導致二義性,因為編譯器不知道應該使用哪個 Base 子物件的成員。

      • 但使用虛擬繼承後,DiamondDerived 只擁有一個共同的 Base 子物件,它來自於 VirtualDerived1 和 VirtualDerived2 中的 Base。這樣就解決了二義性問題,因為只有一個 Base 子物件可供存取。

      • 虛擬繼承確保了在多重繼承中共享的基礎類別只有一個『實例』,從而解決了二義性問題。

    #include <iostream>
    using namespace std;
    
    class A {
    public:
        A() { cout << "Constructing A" << endl; }
        void show() { cout << "A::show" << endl; }
    };
    
    class B : virtual public A {
    public:
        B() { cout << "Constructing B" << endl; }
    };
    
    class C : virtual public A {
    public:
        C() { cout << "Constructing C" << endl; }
    };
    
    class D : public B, public C {
    public:
        D() { cout << "Constructing D" << endl; }
    };
    
    int main() {
        D obj;
        obj.show(); // 不再二義性
        return 0;
    }
  • 菱形繼承(Diamond Inheritance)本身並不一定是設計錯誤;真正要判斷的是,最終衍生類別應該擁有「一份」還是「多份」共同基底類別的狀態。

  • C++ 的虛擬繼承(Virtual Inheritance)不是單純用來消除編譯錯誤,而是在決定物件的組成結構(Object Structure)。

    
    #include <iostream>
    using namespace std;
    
    class Counter {
    public:
      int count = 0;
    
      void increment() {
        ++count;
      }
    };
    
    class BuySide : public Counter {};
    
    class SellSide : public Counter {};
    
    class TradingStatistics :public BuySide, public SellSide {};
    
    int main() {
        TradingStatistics stats;
        stats.BuySide::increment();
        stats.BuySide::increment();
        stats.SellSide::increment();
    
        cout << "Buy: "
                  << stats.BuySide::count << "\n";
    
        cout << "Sell: "
                  << stats.SellSide::count << "\n";
    }
    
    // Buy: 2
    // Sell: 1
  • Virtual Inheritance 與 Virtual Function 是兩個不同概念。

    項目 Virtual Function Virtual Inheritance
    中文 虛擬函數 虛擬繼承
    主要用途 實現執行時期多型 共享虛擬基底子物件
    語法位置 成員函數宣告 基底類別繼承宣告
    解決問題 動態分派 菱形繼承中的重複基底子物件
    是否為相同機制 否 否
    • 因為 BuySide 和 SellSide 都是非虛擬繼承 Counter。

    • 因此,在 TradingStatistics 物件內,會存在兩份獨立的 Counter 子物件。

    • 因此,這個情況下不使用 Virtual Inheritance,反而符合需求。

    • 不過,仍然存在名稱歧義:

      • stats.count;  // 編譯錯誤:不知道是哪個 Counter

        必須明確指定:

        stats.BuySide::count;
        stats.SellSide::count;

        所以,保留兩份基底子物件雖然可能是正確設計,但仍需要妥善處理介面歧義。

    • 如果需求改變,我們希望統計的是買進與賣出的總交易次數,而不是分別維護兩個計數器。

      那麼,就可能需要讓兩條繼承路徑共用同一個 Counter。

      
      #include <iostream>
      using namespace std;
      
      class Counter {
      public:
        int count = 0;
      
        void increment() {
          ++count;
        }
      };
      
      // 改成虛擬繼承
      class BuySide : virtual public Counter {};
      
      class SellSide : virtual public Counter {};
      
      class TradingStatistics :public BuySide, public SellSide {};
      
      int main() {
            TradingStatistics stats;
            stats.BuySide::increment();
            stats.BuySide::increment();
            stats.SellSide::increment();
      
            cout << "BuySide Counter: "
                    << stats.BuySide::count << "\n";
      
            cout << "SellSide Counter: "
                    << stats.SellSide::count << "\n";
      
            cout << "Total: "
                    << stats.count << "\n";
      }
      
      // BuySide Counter: 3
      // SellSide Counter: 3
      // Total: 3
  • C++ 標準函式庫:

    std::istream
    std::ostream
    std::iostream
    • 其中 std::iostream 同時結合輸入與輸出串流功能,而 std::istream 與 std::ostream 透過虛擬繼承共享共同的 std::basic_ios 基底子物件。

    • 這是有明確理由採用虛擬繼承的經典案例。

  • 菱形繼承不一定需要使用虛擬繼承。

    • 在一般非虛擬繼承下,若衍生類別透過不同路徑繼承同一個基底類別,最終物件會包含多份獨立的基底類別子物件。

    • 如果設計需求就是保留各條繼承路徑的獨立資料與狀態,非虛擬繼承可能是合理選擇。

    • 相反地,如果不同繼承路徑在語意上應代表同一個共同基底實體,就可以使用虛擬繼承,讓最終物件只保有一份共享的虛擬基底子物件。

    • 因此,是否使用虛擬繼承,應根據物件身分、狀態共享需求與類別設計語意決定,而不應只以是否出現名稱歧義作為判斷標準。

多態與多重繼承

  • 多態:行為上的概念,著重於如何透過統一的介面(如父類別指標或參考)來操作多個子類別物件。

  • 多態:同一個操作作用在不同的物件上,將產生不同的執行結果。

  • 多重繼承:結構上的概念,讓類別可以從多個來源繼承屬性和行為。

  • 在多重繼承中,多態仍然適用。例如:

    • 如果一個子類別繼承了多個父類別,而父類別中有虛函數,則多態可以根據子類別的覆寫來實現動態行為。
    #include <iostream>
    using namespace std;
    
    class Animal {
    public:
        virtual void makeSound() const {
            cout << "Generic Animal Sound" << endl;
        }
        virtual ~Animal() {}
    };
    
    class Flyable {
    public:
        virtual void fly() const {
            cout << "Flying in the sky" << endl;
        }
        virtual ~Flyable() {}
    };
    
    class Bat : public Animal, public Flyable {
    public:
        void makeSound() const override {
            cout << "Bat sound" << endl;
        }
        void fly() const override {
            cout << "Bat flying" << endl;
        }
    };
    
    int main() {
        Bat bat;
        Animal* animal = &bat;
        Flyable* flyable = &bat;
    
        animal->makeSound(); // 輸出: Bat sound
        flyable->fly();      // 輸出: Bat flying
    
        return 0;
    }
    #include <iostream>
    using namespace std;
    
    // 父類別: Animal
    class Animal {
    public:
        virtual void makeSound() const {
            cout << "Generic Animal Sound" << endl;
        }
        virtual ~Animal() {}
    };
    
    // 中間類別: Mammal 和 Bird,使用虛擬繼承
    class Mammal : virtual public Animal {
    public:
        virtual void walk() const {
            cout << "Mammal walking" << endl;
        }
    };
    
    class Bird : virtual public Animal {
    public:
        virtual void fly() const {
            cout << "Bird flying" << endl;
        }
    };
    
    // 最終類別: Bat,繼承 Mammal 和 Bird
    class Bat : public Mammal, public Bird {
    public:
        void makeSound() const override {
            cout << "Bat sound: Screech" << endl;
        }
        void walk() const override {
            cout << "Bat crawling" << endl;
        }
        void fly() const override {
            cout << "Bat flying swiftly" << endl;
        }
    };
    
    int main() {
        Bat bat;
    
        // 多態: 透過父類別指標呼叫覆寫方法
        Animal* animalPtr = &bat;
        animalPtr->makeSound(); // 輸出: Bat sound: Screech
    
        Mammal* mammalPtr = &bat;
        mammalPtr->walk();      // 輸出: Bat crawling
    
        Bird* birdPtr = &bat;
        birdPtr->fly();         // 輸出: Bat flying swiftly
    
        return 0;
    }

設計模式- 單例(Singleton) 模式

  • Singleton 模式 用於「保證某個類別在整個程式中只有一個實例」,並提供一個全域、受控的存取介面。

  • 用於只能創建只能產生一個物件(實例)的的類別。

  • 在 QuantLib,用來管理全局唯一的資源,如市場資料或設定。

  • 實現細節:

    • 核心類別:

      • Settings:管理全局設定。
    • 檔案位置:

      • ql/settings.hpp
    • 使用案例:

      • 使用 Settings::instance() 取得全局設定物件,例如評價日期(evaluation date)。

      • 瞭解 QuantLib 中 Settings 類別如何使用單例模式管理全局設定。

      • 繼承關係:Settings : public Singleton<Settings>

      • 避免多個設定實例造成混亂

      • 在 QuantLib 中,可透過 Settings::instance() 取得單例物件,進而操作上述設定。

  • Settings 類別用來管理執行時的全局設定,主要功能包括:

    • 評價日期 (Evaluation Date): 控制計算時的基準日期,允許設置、讀取和監控其變化。

    • 參考日期事件 (Reference Date Events): 決定是否將發生在參考日期的事件視為已發生。

    • 今日現金流量 (Today’s Cash Flows): 指定是否將當天的現金流量計入 NPV。

    • 歷史修正值 (Historical Fixings): 設定是否強制應用當天的歷史修正值。

  • 單例(Singleton) 範例 1:簡化版單例(GameConfig)

    #include <iostream> // 引入標準輸入輸出庫
    
    // 定義 GameConfig 類別,實現單例模式
    class GameConfig
    {
    private:
      // 私有的構造函數,防止外部直接建立物件
      GameConfig()
      {
        std::cout << "GameConfig created!" << std::endl;
      }
    
      // 禁止使用複製構造函數
      GameConfig(const GameConfig &) = delete;
    
      // 禁止使用賦值運算符
      GameConfig &operator=(const GameConfig &) = delete;
    
      // 私有的解構函數,確保靜態變數在程式結束時釋放
      ~GameConfig()
      {
        std::cout << "GameConfig destroyed!" << std::endl;
      }
    
    public:
      // 提供唯一的全域存取點,回傳單例實例
      static GameConfig &getInstance()
      {
        // 靜態局部變數,只有第一次呼叫時初始化,之後都回傳同一個實例
        static GameConfig instance;   // 這一行位於 getInstance() 函數內,代表的是 函數內的靜態區域變數。
        return instance;
      }
    
      // 公有成員函數,顯示單例實例的記憶體地址
      void showMessage() const
      {
        std::cout << "GameConfig instance address: " << this << std::endl;
      }
    };
    
    int main()
    {
      // 取得單例實例的第一個引用,並呼叫 showMessage() 確認地址
      GameConfig &config1 = GameConfig::getInstance();
      config1.showMessage(); // 預期輸出: GameConfig instance address: [實例地址]
    
      // 取得單例實例的第二個引用,並再次呼叫 showMessage() 確認地址
      GameConfig &config2 = GameConfig::getInstance();
      config2.showMessage(); // 預期輸出與 config1 的地址相同
    
      // 回傳 0,結束程式
      return 0;
    }
    • 唯一性:限制類別只能有一個實例(透過私有建構子)。

    • 控制存取:提供一個全域的存取點。

    • 延遲初始化:只有在第一次需要時才會建立實例。

      • 將構造函數設為私有,防止外部使用 new 或其他方式建立實例。

      • 作用:限制實例的建立,確保只能透過 getInstance() 取得實例。

    • 禁止複製與賦值

      • 刪除複製構造函數與賦值運算子,防止透過複製或賦值生成新的實例。

      • 作用:保證單例模式的唯一性。

        private:
          GameConfig() { ... }
          GameConfig(const GameConfig &) = delete;
          GameConfig &operator=(const GameConfig &) = delete;
      • 建構函數設為 private:不讓外部呼叫 new GameConfig(),也不能寫 GameConfig config;。靜態方法 getInstance()。

      • 刪除拷貝建構子與賦值運算子:避免 GameConfig config2 = config1; 這種方式複製出第二個實例。

    • 靜態局部變數:static GameConfig instance 保證 instance 只會被初始化一次。

      • 記憶體管理:靜態局部變數在程式結束時自動釋放。

      • 執行流程:

      • 第一次呼叫 getInstance() 時,創建 instance。

      • 後續呼叫直接回傳已經存在的 instance。

    • 第一次呼叫 getInstance():

      1. 靜態變數 instance 被初始化。

      2. 輸出 "GameConfig created!"。

      3. config1 取得 instance,呼叫 showMessage(),輸出實例記憶體位址。

    • 第二次呼叫 getInstance():

      1. 靜態變數 instance 已經存在,直接回傳。

      2. config2 與 config1 指向相同實例,呼叫 showMessage(),再次輸出相同記憶體位址。

    • 程式結束:

      1. instance 的解構函數被呼叫,輸出 "GameConfig destroyed!"。
  • 使用範例(日誌記錄器)– 多執行緒單例應用(Logger):

    #include "ql/patterns/singleton.hpp"
    #include <thread> // 用於模擬多執行緒
    #include <iostream>
    #include <fstream>
    #include <string>
    #include <mutex>
    
    class Logger : public QuantLib::Singleton<Logger>
    {
      // 因為 QuantLib::Singleton 模板類別需要存取子類別 Logger 的私有構造函數,以實現單例模式。
      friend class QuantLib::Singleton<Logger>; // 允許 Singleton 存取 Logger 的私有構造函數
    
    private:
      std::ofstream logFile; // 用於寫入日誌的檔案流
      std::mutex logMutex;   // 用於保證多執行緒環境下的日誌安全
      Logger()
      {
        // 初始化日誌檔案
        logFile.open("log.txt", std::ios::app);
        if (logFile.is_open())
        {
          logFile << "Logger initialized." << std::endl;
        }
        else
        {
          std::cerr << "Failed to open log file!" << std::endl;
        }
      }
    
      ~Logger()
      {
        if (logFile.is_open())
        {
          logFile << "Logger shutting down." << std::endl;
          logFile.close();
        }
      }
    
    public:
      // 寫入日誌訊息
      void log(const std::string &message)
      {
        // 自動「上鎖 (lock)」:阻止其他執行緒同時進入。當函數結束時,自動「解鎖 (unlock)」。
        // 當 Thread 1 在寫入時,Thread 2 和 Thread 3 必須等它完成,寫入順序雖然不一定固定,但內容絕不會混亂。
        std::lock_guard<std::mutex> guard(logMutex); // 確保多執行緒安全
        if (logFile.is_open())
        {
          logFile << message << std::endl;
        }
        else
        {
          std::cerr << "Log file is not open!" << std::endl;
        }
      }
    };
    
    
    // 當多個執行緒執行 workerFunction 時,每個執行緒都會執行這個迴圈五次,對日誌記錄器進行多次的 log 呼叫。
    void workerFunction(int id)
    {
      Logger &logger = Logger::instance(); // 產生單例實例
      for (int i = 0; i < 5; ++i)
      {
        logger.log("Thread " + std::to_string(id) + " - Log message " + std::to_string(i));
      }
    }
    
    
    int main()
    {
      // 主執行緒寫入日誌
      Logger &logger = Logger::instance();   // 建立單例。
      logger.log("Main thread - Application started");
    
      // 啟動多個工作執行緒,模擬平行日誌寫入
    
      std::thread t1(workerFunction, 1);
      std::thread t2(workerFunction, 2);
      std::thread t3(workerFunction, 3);
    
      // 等待所有執行緒完成
      // join() 的意思是:「等待子執行緒全部執行完畢,主執行緒才繼續」。
      t1.join();
      t2.join();
      t3.join();
    
      logger.log("Main thread - Application shutting down");
    
      return 0;
    }
    • 展示如何在多執行緒環境下使用單例模式的 Logger 類別進行日誌記錄。

    • 透過使用 std::mutex 保證日誌寫入的執行緒安全,確保多個執行緒同時寫入日誌時不會發生競爭條件。

      • std::mutex 是 C++11 標準程式庫中提供的互斥鎖(mutex)類別,用於多執行緒環境中保護共享資源,避免資料競爭(race condition)。主執行緒和工作執行緒都可以安全地記錄日誌訊息,並且所有訊息都會被寫入同一個日誌檔案。
    • 私有成員

      • std::ofstream logFile;:

        • 用於寫入日誌的檔案流。

        • 共用資源 → 多個執行緒都會用。

      • std::mutex logMutex;:

        • std::mutex 是 鎖(Lock) → 確保一次只有一個執行緒能寫。

        • 用於保證多執行緒環境下的日誌寫入安全。

      • 構造函數和解構函數

        • Logger():私有的構造函數,初始化日誌檔案。如果檔案成功打開,寫入 “Logger initialized.” 訊息;否則,輸出錯誤訊息到標準錯誤流。

        • ~Logger():私有的解構函數,在日誌檔案關閉前寫入 “Logger shutting down.” 訊息。

    • 友類別宣告

      • friend class QuantLib::Singleton<Logger>;:允許 QuantLib::Singleton 存取 Logger 的私有構造函數,以實現單例模式。
    • 使用 std::lock_guard<std::mutex> 確保在多執行緒環境下的日誌寫入操作是安全的。

    • 如果日誌檔案已打開,將訊息寫入檔案;否則,輸出錯誤訊息到標準錯誤流。

    • 在 main 函數中,首先取得 Logger 的單例實例,並記錄應用程式啟動的訊息。

    • 接著,啟動三個工作執行緒 t1, t2, t3,每個執行緒都執行 workerFunction 函數,模擬平行日誌寫入。

    • 使用 join 函數等待所有執行緒完成,確保主執行緒在所有工作執行緒完成後才繼續執行。

    • 最後,記錄應用程式關閉的訊息,並結束程式。

    • 使用 Singleton → 所有執行緒共用同一個 Logger 物件。

    • C++11 的 static 保證 Singleton 在多執行緒中建立時也是安全的(只初始化一次)。