類別(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
    {
      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 "utilities.h"
    #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 myTime4_1 = {myTime};
  • 『複製建構函數(Copy Constructor)』

    • 定義:複製建構函數是一種建構函數,當物件以『值傳遞』方式從另一個物件複製時被呼叫。它用於創建一個物件的副本。

    • 特點

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

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

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

    • 『複製建構函數(Copy Constructor)』的基本語法涉及以下幾個要素:

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

      • 單一參數:通常是對同一類別物件的『參考』(通常是 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);               // 回傳物件本身的函數。
    
    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 可以直接串接執行。
  • 在賦值運算子(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 成員變數』可以被正常使用。

  • 定義與初始化:為了分配儲存空間,static 成員變數需要在『類別外部』進行定義和初始化(除了 const static 整數型態成員和 constexpr 成員,這些可以在『類別內』部直接初始化)。

  • 生命週期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 成員函數 是屬於類別本身,而不是任何特定物件的函數。

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

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

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

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

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

  • this 指標:在 static 成員函數內部,沒有 this 指標。因為 this 指標變數是用於指向當前物件的實例,而 static 函數不依賴於任何實例。

  • 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;
    }

類別初始化、預設建構函數、“= 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關鍵字後面接運算子

複製賦值運算子

  • 範例:

    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;
    }

子類別、呼叫順序,存取等級與函數覆寫

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

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

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

  • 定義子類別語法: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)

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

      • 函數覆寫(function override)的規則

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

        • 如果父類別中的函數被標記為 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 成員。

函數遮蔽 (Function Hiding)

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

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

    // 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)動態綁定。 否,函數遮蔽是靜態綁定,完全取決於編譯時的類型資訊。
    是否受參數影響 子類別函數簽名(名稱、參數、回傳值)需完全匹配父類別虛擬函數。 子類別函數名稱相同即可,與參數和回傳值無關。
    行為影響 覆寫後,當透過父類別指標或參考呼叫函數時,執行的是子類別版本的函數。 遮蔽後,子類別中的同名函數會隱藏父類別的所有同名函數,導致父類別的函數無法直接被子類別使用。
    使用情境 用於實現多態行為,讓子類別提供自己的實現來取代父類別的實現。 子類別定義的函數完全取代父類別的函數,或需要不同功能的實現。

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

父類別指標與子類別指標

  • 物件可透過 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;
}

友函數(friend Function)

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

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

  • 特點:

    • 存取權限:友函數可以存取該類的『所有成員』,包括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 被宣告為友函數,但它仍然是一個全局函數,可以在全局範圍內被調用。

多重繼承(multiple inheritance)

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

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

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

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

  • 語法:

    class Derived : public Base1, public Base2 {
        // ...
    };
  • 菱形繼承問題:

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

      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 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;
    }

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

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

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

  • 實現細節

    • 核心類別:

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

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

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

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

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

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

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

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

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

  • 單例(Singleton) 模式説明案例:

    #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;
        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() 取得實例。

    • 禁止複製與賦值

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

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

    • 靜態方法 getInstance()

    • 靜態局部變數: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!"
  • 使用範例(日誌記錄器)

    #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)
      {
        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);
    
      // 等待所有執行緒完成
      t1.join();
      t2.join();
      t3.join();
    
      logger.log("Main thread - Application shutting down");
    
      return 0;
    }