講義六

類別(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 修飾符提供各種實現類別的功能,但不曝露給外部使用者存取

  • publicprivate 存取修飾符

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

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

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

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

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

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

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

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

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

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

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

    • 按慣例可將『無』成員函數只有成員變數的資料結構定義為 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;
  • 物件複製後,每個物件都有『不同』的記憶體位址(互相獨立,互不影響),且每個物件的成員變數值都相同。

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

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

    • =

    • ()

    • {}

    • ={}

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

私有成員

  • 類別的『私有(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)

        • 定義:在淺複製中,物件的所有成員都會逐位元複製。如果成員是『指標變數』,則複製的是指標變數的『值』(即記憶體地址),而非指標所指向的值。

        • 問題

          • 共享相同記憶體

            • 複製後的物件和原始物件將共享相同的動態分配記憶體。

            • 可能導致一個物件釋放記憶體後,另一個物件仍嘗試存取該記憶體。

          • 重複釋放:當兩個物件都嘗試釋放同一塊記憶體時,會導致執行時錯誤。

        • 使用情境:當類別中沒有『指標』或動態分配資源時,淺複製通常是安全的。

      • 深複製(Deep Copy)

        • 定義

          • 深複製』是對物件進行完全獨立的複製

          • 不僅複製物件的值和指標值,『還包括指標變數所指向的資料』

          • 通常需要在『複製建構函數』中手動實現

        • 被深複製所得之新物件擁有與原物件『完全獨立』的資源,不會因為一個物件的改變而影響到另一個物件。

        • 實現

          • 獨立記憶體:其替指標所指向的數據在『堆(heap)』上分配新的記憶體,並從原物件複製數據。

          • 避免共享和重複釋放:這樣每個物件都有自己的獨立記憶體,避免了共享與重複釋放記憶體的問題。

        • 使用情境:當類別包含指標或其他需要手動管理資源時,建議使用『深複製』來確保資源的正確管理。

        #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:這部分程式碼存取傳入物件 otherdata 成員。由於 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 成員相同的值。

多個建構函數

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

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

  • 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);         // 錯誤。

隱性轉換與 explict

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

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

    // 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 物件(臨時物件),函數呼叫完畢後,物件 myt 的生存期結束,所佔用的資源則被系統回收。

    Time myTime100 = {16};   // 明確讓系統呼叫帶一個參數的建構函數。
    Time myTime101 = 16;     // 程式碼較模糊不清,存在『隱性轉換』的問題。
    func(16);                // 程式碼較模糊不清,存在『隱性轉換』的問題。
  • 因此,可在建構函數的『宣告』加上 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++中,建構函數『初始化列表』是用於在建構函數體執行前初始化類的成員變數的一種方法。它為『非基本類型』(類別型態)的成員變數提供了一種更高效的初始化方式。

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

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

  • 『初始化列表』的執行是在函數體執行『之前』就進行。

    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 成員不能在建構函数體内對它們進行賦值操作)。

      • 補充:

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

        • 『參考』成員選擇使用 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 類包含四個成員:idconst int),mileageint 參考),modelstd::string 物件),和 engineEngine 類別的物件)。

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

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

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

    • 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:為『自身物件的指標變數』,其指向物件本身。

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

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

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

  • static 成員函數無法使用 this

    • static 成員函數是類別層級的,與某個具體物件無關,因此沒有 this 指標
  • 當物件呼叫成員函數時,編譯器會將呼叫此成員函數的物件的位址傳遞給該成員函數的『隱藏』形式參數,其名為『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&)的函數』。
    
    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 指標變數只能在『成員函數』中使用,全局變數、『靜態函數』等都不能使用 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 staticconstexpr,就可以直接在類別內初始化,不需要在 .cpp 檔定義:

    // C++17 起可這樣寫:
    class MyClass {
    public:
        inline static int counter = 0;
        static constexpr double pi = 3.1415926;
    };
  • 生命週期static 成員變數的生命週期『從程式開始到程式結束』

  • 儲存類型:其儲存在程式的數據區域,而『非』堆(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 成員變數(函數)』:

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

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

static 成員函數

  • 成員函數也可以在前面加上 static 關鍵字來修飾,則為 『static 成員函數』。

  • static 成員函數 是屬於『類別』本身,而『非』任何特定物件的函數。

  • 不需 this 指標即可執行。

  • 常見於工具類、轉換邏輯、靜態計算公式

  • 獨立於實例(物件)static 成員函數可以在沒有類別的任何實例下調用

  • 通常用於全局功能,例如計數物件的數量、提供公用的工具函數等。

  • static 成員函數』是透過『類別名稱』而『非』透過類別的物件來調用的。

  • static 成員函數』只能操作和類別有關的成員變數,也就是 static 成員函數只能操作類別的『 static 成員變數』和其他 『static 成員函數』。

  • 無法存取類別『非 static 』成員變數或函數(因為這些成員需要類別的實例)。

  • this 指標:在 static 成員函數內部,沒有 this 指標。因為 this 指標變數是用於指向當前物件的實例,而 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 原因
所有物件共用 共用資源
不需要存取成員變數 工具邏輯
只想建立一個實例 單例模式
定義類別級常數 常用定義

類別初始化、預設建構函數、“= 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;
    }

重載運算子

  • 運算子重載允許自訂類別使用類似內建型別的運算符操作。

  • 運算子種類繁多,如 ==>>=<<=!=等。

  • 若兩個物件要進行是否相等的比較,當『未』重載 == 運算子時,因為系統不知道兩個物件的相等比較要如何進行,則會發生編譯錯誤。

// 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);

複製賦值運算子

  • 範例:

    Time myTime;                // 呼叫預設建構函數(不帶參數)。
    Time myTime2 = myTime       // 呼叫複製建構函數。
    Time myTime5 = {myTime};    // 呼叫複製建構函數。
    
    Time myTime6;               // 呼叫預設建構函數(不帶參數)。
    myTime6 = myTime5;          // 賦值運算子,並非呼叫複製建構函數。
  • 若對物件賦值,系統會呼叫一個複製賦值運算子。

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

    Time& operator=(const Time&);            // 賦值運算子重載。

解構函數(釋放函數)

  • 解構函數(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::vectorstd::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;
    }
    • 程式會先執行『父類別』的建構函數,再執行『子類別』的建構函數。

存取等級(publicprotectedprivate

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

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

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

繼承等級(publicprotectedprivate

  • 子類別 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                    // 錯誤:子類別無存取權限。
  • 子類別可以存取父類別中的 publicprotected 成員,並可以進一步將它們公開或保護。

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

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

    • 函數覆寫(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

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

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

  • 私有繼承(private

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

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

  • 保護繼承(protected

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

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

函數覆寫(Function Override)

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

    • 函數覆寫(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)

  • 一般情況下,父類別的成員函數只要是用 publicprotected 修飾的,子類別只要不使用 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 關鍵字 讓編譯器幫你檢查覆寫是否正確

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

父類別指標與子類別指標

  • 物件可透過 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 指標並讓它指向 DogCat 的物件時,呼叫 speak 函數會根據物件的實際型態(DogCat)來決定調用哪個 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),而子類別根據不同模型或產品「覆寫」這些虛函數以提供具體實作。

  • InstrumentBondFixedRateBond (檔案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()) 來對不同債券類別運作。

  • YieldTermStructureFlatForward, 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)是一種特殊的函數,它被允許存取一個類別的 privateprotected 成員。

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

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

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

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

  • 特點:

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

    • 非成員函數:友函數不是類別的成員函數,因此它不是由物件來調用的。它就像一般的函數一樣,但有存取特定類別的privateprotected成員的能力。

    • 宣告方式:在『類別的定義內』使用 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)
    }
  • 所有函數(成員或非成員)都在程式的「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。這是因為 BA 的友類別,即使 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 看不到 Bprivate)。

    #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:這個類有兩個私有成員,namesalary,以及一個公共方法 displayInfo() 用來顯示員工的基本信息。
    • Manager:這個類有兩個方法:
      • giveRaise():該方法可以給員工加薪,這是因為 ManagerEmployee 類的友類別,可以直接存取員工的私有成員 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)

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

多重繼承(multiple inheritance)

  • 多重繼承允許一個類別同時繼承自多個父類別。

  • C++ 的子類別可以繼承多個父類別的屬性和方法。

  • 多重繼承以將不同類別的功能結合到一個子類別中,但也可能引發一些設計上的問題,例如命名衝突和模糊性問題(『菱形繼承(diamond inheritance)』)

  • 許多程式語言支援多重繼承,例如C++ 與 Python,但在其他程式語言(如 Java)則不支援。

  • 語法:

    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++ 對多重繼承物件的記憶體佈局是由編譯器決定的,父類別的繼承順序可能會影響記憶體分配和存取效率。
  • 菱形繼承問題:

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

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

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

    • 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++中用於解決多重繼承二義性問題的概念。

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

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

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

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

  • 使用虛擬繼承時,可以在基礎類別的繼承前加上 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 同時繼承自 VirtualDerived1VirtualDerived2

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

      • 但使用虛擬繼承後,DiamondDerived 只擁有一個共同的 Base 子物件,它來自於 VirtualDerived1VirtualDerived2 中的 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;
    }

多態與多重繼承

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

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

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

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

    • 如果一個子類別繼承了多個父類別,而父類別中有虛函數,則多態可以根據子類別的覆寫來實現動態行為。
    #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)。

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

      • 繼承關係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. config2config1 指向相同實例,呼叫 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 在多執行緒中建立時也是安全的(只初始化一次)。