class)類別就是使用者『自定義』的資料型態,可將類別視為一個『命名空間』,並包含一些有用的內容(如:成員函數(member function)、成員變數(member variable))。
類別(Class):定義了一組屬性(成員變數)和方法(成員函數)的『藍圖』。
物件(Object):根據類別創建的實例(實體)。
繼承(Inheritance):可用來避免程式碼重複撰寫與降低維護困難度的機制,意指一個類別繼承另一個類別的特性(含屬性與方法)。
封裝(Encapsulation):將資料和操作資料的程式碼包裝在一起。
多態(又稱多態,Polymorphism):允許使用共同的介面來表現不同行為的基礎模式。多態性使得相同的介面可以用於多種不同的具體實現,從而提高了程式碼的靈活性和擴展性。
『類別』,只有在 C++ 才有,C 語言中沒有『類別』的概念。
實例化(Instantiation):
類別本身只是一個『藍圖』,當創建類別的實例(即物件)時,才會在記憶體中分配實際的空間。
依據『藍圖』,每個『實例』都擁有類別定義的屬性和方法。
類別的基本思想:
資料抽象(data abstraction):介面與實作的隔離。
介面(interface):是指外部使用者所能執行的操作。
實作(implementation):含成員變數、負責介面實作的函數(public)以及定義類別所需的私有(private)函數。
封裝(encapsulation):隱藏類別的實作細節,也就是類別使用者(只能使用介面提供的功能)無法使用的部分。
C++ 內的每個變數都有型態(type)。
在 C++ 中,可定義一個屬於某一個類別的變數(可稱為:實例化),稱其為『物件(object)』。
類別(『父類別』,又稱『基礎類別』)可以衍生出『子類別』(又稱『衍生類別』)。
類別本身就是一個『作用域(scope)』。
以 物件名.成員名 來存取成員。
若以該物件的『指標』進行存取,則使用
指標名 -> 成員名。
struct student // struct內的成員皆為public。C++ 也有struct。
{
int number;
char name[100];
void func(){};
};
student student1;
student1.number = 1001;
std::strcpy(student1.name, "Zack");
student1.func();
student *pstudent1 = &student1;
pstudent1->number = 1005;
std::cout << student1.name << std::endl;
std::cout << student1.number << std::endl;類別中 public
修飾符所修飾的成員提供類別與外界存取的『介面(interface)』,而
private
修飾符提供各種實現類別的功能,但不曝露給外部使用者存取。
public 與 private 存取修飾符
public:公開。用此修飾符修飾的成員可以供外部使用。可視為結構或類別的『介面』。
private:私有。用此修飾符修飾的成員,只能被該結構或類別內部定義的成員函數使用。
struct(結構)成員預設為
public。
struct 結構名 { ... };class(類別)成員預設為
private。
語法:class 類別名 { ... };(有分號
;)
等價於:struct 結構名 {private: ... };(有分號
;)
『定義』在『類別內部』的函數爲『隱性』的
inline 函數。
一般來說,類別的『定義』寫在標頭檔(.h),而標頭檔的檔名一般與類別名相同。
類別的實作程式碼則放在一個或多個原始檔(.cpp)中,此原始檔檔名一般也與類別名相同。
若有任何其他原始檔(.cpp)欲使用某個類別時,需在其原始檔的開頭
#include
該類別定義相關的『標頭檔(.h)』。
建議不要將 class 與 struct
混用,否則程式碼會顯得較為混亂。
按慣例可將『無』成員函數只有成員變數的資料結構定義為
struct。
而把『有』成員函數的資料結構定義為 class 。
// 定義Time類別
class Time
{
public:
int Hour;
int Minute;
int Second;
void initTime(int tmpHour, int tmpmin, int tmpsec) // 成員函數
{
Hour = tmpHour;
Minute = tmpmin;
Second = tmpsec;
}
};
int main(int argc, char const *argv[])
{
Time myTime;
myTime.initTime(11, 14, 5); // 使用成員函數
std::cout << myTime.Hour << std::endl; // 11
std::cout << myTime.Minute << std::endl; // 14
std::cout << myTime.Second << std::endl; // 5
return 0;
}
將類別『定義』與『實作』分別放在標頭檔(MyTime.h)與原始檔(MyTime.cpp)內:
// MyTime.h
#ifndef __MyTime___
#define __MyTime___
class Time
{
public:
int Hour;
int Minute;
int Second;
void initTime(int tmpHour, int tmpmin, int tmpsec); // 成員函數的宣告。
};
#endif
// MyTime.cpp
#include "MyTime.h"
void Time::initTime(int tmpHour, int tmpmin, int tmpsec) // 『Time::』 表示該函數屬於Time類別
{
Hour = tmpHour;
Minute = tmpmin;
Second = tmpsec;
}注意:類別可以定義多次(但其他如函數,或變數只能定義一次,故類別的定義比較特殊),故類別定義可以放在標頭檔,且多個.cpp檔案都可以包含該標頭檔。
Time myTime;
myTime.initTime(11, 14, 5);
// 物件複製:
Time myTime2 = myTime;
Time myTime3(myTime);
Time myTime4{myTime};
Time myTime5 = {myTime};
Time myTime6;
myTime6 = myTime5;
物件複製後,每個物件都有『不同』的記憶體位址(互相獨立,互不影響),且每個物件的成員變數值都相同。
物件複製:指定義一個新物件時,是用另外一個物件內的『內容』來進行『初始化』。也是就每個成員變數逐個複製。
語法:透過『賦值運算子 = 』:
=
()
{}
={}
可透過為類別來定義『賦值運算子 =
』來控制物件的複製行為。
類別的『私有(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:這部分程式碼存取傳入物件
other 的
data 成員。由於
other 是一個對
MyClass 物件的『參考』,因此
other.data 指向
other 物件中的動態分配整數。
*other.data:在這裡,*
是解參考運算子,取得 other.data
指標所指向的實際整數值。
new int(...):new
是一個動態記憶體分配運算符。它在『堆(stack)』上為一個整數分配記憶體,並回傳這塊新分配記憶體的位址。在括號內,將
*other.data 的值傳遞給
new int,這樣就在『堆(heap)』上創建一個新的整數,其值與
other.data 所指向的整數相同。
data = ...:最後,將
new int(...)
回傳的新記憶體地址賦值給當前物件的 data
指針。這樣,當前物件的 data
指標就指向了一塊新的、僅屬於此物件的記憶體空間,這塊記憶體包含了與
other 物件的
data 成員相同的值。
類別中可存在多個『建構函數』,其意指可為該類別物件提供多種創建物件的方法(函數重載)。
多個『建構函數』具有『參數數量』與『參數型態』的差異。
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隱性轉換:例如:當 int 與 double
做運算時,編譯器會將 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
類包含四個成員:id(const
int),mileage(int
參考),model(std::string
物件),和
engine(Engine
類別的物件)。
id 作為
const
成員,在建構函數初始化列表中被初始化。
mileage
是一個『參考』成員,在建構函數初始化列表中被初始化。
model 和
engine
是『物件』成員,它們也在『建構函數初始化列表』中被初始化。
Engine
類別是一個簡單的類別,包含一個 std::string
成員 type,用於表示引擎的型號。
在 main 函數中,創建了一個
Car 物件
myCar,並透過
display 方法展示其訊息。
inline 成員函數直接寫在『類別定義』內實作的成員函數會被當作
inline 函數做處理。
inline
函數:編譯器會嘗試將函數體內的程式碼直接取代函數呼叫的程式碼,可提高程式執行的效率。
inline 函數只是對編譯器的建議,能否
inline
成功,取決於編譯器。當成員函數的定義簡單,可提高函數內聯成功的機率。
#ifndef __MyTime___
#define __MyTime___
#include <iostream>
class Time
{
public:
int Hour;
int Minute;
int Second;
explicit Time();
Time(int tmphour);
explicit Time(int tmphour, int tmpmin, int tmpsec);
//////////////////////////////////////////////////////////////////////////
void addHour(int tmphour) // inline成員函數。
{
Hour += tmphour;
}
/////////////////////////////////////////////////////////////////////////
private:
int Millisecond;
void initMilliTime(int mls);
};
#endif /* __MyTime___ */const作用:代表該成員函數『無法』修改該物件的任何成員變數之值。
注意:若想在成員函數末尾加上
const,必須在函數『宣告』與『定義』加入
const。
該函數被稱為:『const 成員函數』。
『const 成員函數』可被 『const
物件』或『非 const 物件』所調用。
可在 MyTime.h 加入一個新的成員函數:
void noone() const // 成員函數末尾加上 const。
{
Hour += 10; // 錯誤:const成員函數不可以修改成員變數值。
}
#ifndef __MyTime___
#define __MyTime___
#include <iostream>
class Time
{
public:
int Hour;
int Minute;
int Second;
explicit Time();
Time(int tmphour);
explicit Time(int tmphour, int tmpmin, int tmpsec);
void addHour(int tmphour)
{
Hour += tmphour;
}
/////////////////////////////////////////////////////////////////////////////
void noone() const // 成員函數末尾加上 『const』。
{
std::cout << "執行 noone函數" << std::endl;
}
/////////////////////////////////////////////////////////////////////////////
private:
int Millisecond;
void initMilliTime(int mls);
};
#endif /* __MyTime___ */
// tesp.cpp
#include <iostream>
#include "utilities.h"
#include "MyTime.h"
int main(int argc, char const *argv[])
{
const Time abc;
// abc.addHour(12);
abc.noone();
Time def;
def.addHour(12);
def.noone();
return 0;
}普通函數(非成員函數)末尾不能加上
const,無法編譯成功。
mutable成員函數末尾加上
const,是不允許修改成員變數的。但可透過關鍵字
mutable 修飾某成員變數,來修改特定成員變數。
也可以試著將成員函數的 const
拿掉來修改成員變數,但會導致 『const
物件』無法呼叫原來的成員函數。
故可此用關鍵字 mutable
來修飾某一『成員變數』,使得該成員變數處於『可變狀態(mutable)』,即使是在以
const 結尾的成員函數中。
#ifndef __MyTime___
#define __MyTime___
#include <iostream>
class Time
{
public:
mutable int Hour; // 加上mutable修飾符
int Minute;
int Second;
explicit Time();
Time(int tmphour);
explicit Time(int tmphour, int tmpmin, int tmpsec);
void addHour(int tmphour)
{
Hour += tmphour;
}
void noone() const
{
Hour += 3; // 可以用來修改Hour
}
private:
int Millisecond;
void initMilliTime(int mls);
};
#endif /* __MyTime___ */thisthis:為『自身物件的指標變數』,其指向物件本身。
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 static
或 constexpr,就可以直接在類別內初始化,不需要在
.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(...) |
靜態績效計算函數。
|
| 需求 | 是否該用 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;
}
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::vector或std::list)的元素時,當容器被銷毀或元素被刪除時,該元素的解構函數將被自動調用。
class MyResource {
public:
MyResource() {
// 分配資源
}
~MyResource() {
// 釋放資源
}
};
int main() {
MyResource resource; // 創建物件
// 执行其他操作
// 在main函数結束時,MyResource物件的解構函數將被自動調用,
// 以釋放分配的資源
return 0;
} // MyResource物件在這裡超出了作用域,解構函數被自動調用
#include <iostream>
using namespace std;
class Example {
private:
int* data;
public:
// Constructor
Example(int value) {
data = new int(value); // 分配動態記憶體
cout << "Constructor: Memory allocated for " << *data << endl;
}
// Destructor
~Example() {
delete data; // 釋放動態記憶體
cout << "Destructor: Memory deallocated" << endl;
}
void printValue() const {
cout << "Value: " << *data << endl;
}
};
int main() {
Example obj(42); // Constructor called
obj.printValue();
// Destructor will be called automatically here when obj goes out of scope
return 0;
}
#include <iostream>
#include <fstream>
using namespace std;
class FileHandler {
private:
ofstream file;
public:
FileHandler(const string& filename) {
file.open(filename);
if (file.is_open()) {
cout << "File opened: " << filename << endl;
}
}
~FileHandler() {
if (file.is_open()) {
file.close();
cout << "File closed." << endl;
}
}
void write(const string& data) {
if (file.is_open()) {
file << data << endl;
}
}
};
int main() {
FileHandler handler("example.txt");
handler.write("Hello, world!");
// File will be closed automatically when handler goes out of scope
return 0;
}
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base constructor" << endl;
}
~Base() {
cout << "Base destructor" << endl;
}
};
class Derived : public Base {
public:
Derived() {
cout << "Derived constructor" << endl;
}
~Derived() {
cout << "Derived destructor" << endl;
}
};
int main() {
Derived obj;
return 0;
}當有父類別(基礎類別)、子類別(衍生類別)時,這種層次關係為『繼承』,也就是子類別能夠從父類別那裡繼承到許多東西,包括成員變數和成員函數。
先有父類別。父類別主要定義一些『公用』的成員變數與成員函數。接著透過『繼承』此父類別建立新的類別(子類別)。
定義子類別時則可減少許多撰寫程式碼的工作量:只需撰寫子類別獨有的部分即可。
定義子類別語法:class 子類別名:繼承方式 父類別名
子類別可以繼承多個父類別(多重繼承)。
// Human.h
#ifndef __Human__
#define __Human__
class Human
{
public:
Human();
Human(int);
public:
int m_Age;
char m_Name[100];
};
#endif /* __Human__ */
// Humen.cpp
#include "Human.h"
#include <iostream>
Human::Human()
{
std::cout << "執行Human::Human()建構函數" << std::endl;
}
Human::Human(int age)
{
std::cout << "執行Human::Human(int age)建構函數" << std::endl;
}
// Men.h
#ifndef __Men__
#define __Men__
#include "Human.h"
class Men:public Human
{
public:
Men();
};
#endif /* __Men__ */
// Men.cpp
#include "Men.h"
#include <iostream>
Men::Men()
{
std::cout << "執行Men::Men()建構函數" << std::endl;
}
// test.cpp
#include <iostream>
#include "Men.h"
int main(int argc, char const *argv[])
{
Men men;
return 0;
}
public、protected、private)public:可以被任意物件存取。
protected:只允許本類別或『子類別』的成員函數存取。
private:只允許本類別的成員函數存取。
public、protected、private)子類別 public
繼承父類別:父類別所有成員在子類別的存取權限『不會』發生改變。
子類別 protected 繼承父類別:將父類別中
public 成員變成子類別的 protected
成員。父類別中的 protected
成员仍然是子類別的 protected
成員。
子類別
private繼承父類別:使得父類別所有成員在子類別中的存取權限變為
private。
父類別的 private
成員不受繼承方式的影響,『子類別永遠無權存取』。
因此,對於父類別而言,尤其是父類別的成員函數,若不想被外界存取,則設定為
private;若想讓自己的子類別存取,則應設定為
protected。若想公開,則設定為
public。
#ifndef __Human__
#define __Human__
class Human
{
public:
Human();
Human(int);
public:
int m_Age;
char m_Name[100];
protected:
int m_prol;
void funcpro() {};
private:
int m_privl;
void funcpriv();
};
#endif /* __Human__ */
protected 繼承 Humen 類別:class Men:protected Human
{
//
};
Men men;
men.m_Age = 10; // 錯誤:不允許main函數直接存取。
men.m_privl = 15 // 錯誤:子類別無存取權限。子類別可以存取父類別中的 public 和
protected
成員,並可以進一步將它們公開或保護。
子類別無法直接存取父類別中的
private
成員,但可以使用父類別提供的公共函數存取這些私有成員。
子類別可以『覆寫(override)』父類別的
public 和
protected
函數,並可以更改函數的存取等級,但必須符合覆寫的規則。
函數覆寫(override):
子類別可以使用與父類別相同的函數名稱和參數來覆寫父類別的函數。這樣一來,當透過子類別的物件呼叫該函數時,會執行子類別的版本而非父類別的版本。
條件:
父類別的成員函數是『虛函數(virtual)』。
名稱、參數列表、回傳型態(需相容)都要完全相同。
在子類別中實作時,代表「重新定義父類別的行為」。
規則:
函數『名稱』、參數的『型別』及『順序』、『回傳型別』都必須完全相同,否則『不會』被認為是覆寫。
要達到「覆寫(override)」效果,父類別的成員函數必須是
virtual。否則即使子類別定義了同名、同參數、同回傳型態的函數,C++
也只會視為「遮蔽(hiding)」,而不是覆寫。
當父類別中的函數被標記為 virtual,則覆寫時建議使用
override
關鍵字以表明覆寫的意圖,則編譯器可進行檢查,避免覆寫錯誤。
override 是 C++11
引入的『編譯期檢查』關鍵字。若父類別沒有
virtual,但你用了
override,會直接編譯錯誤:
如果參數或回傳型態有任何差異,C++ 不會認為這是覆寫,而可能視為『函數遮蔽(Function Hiding)』。
class Parent {
protected:
int protectedMember; // 父類別的 protected 成員
public:
void setProtectedMember(int value) {
protectedMember = value; // 在父類別中可以存取 protected 成員
}
};
class Child : protected Parent {
public:
void accessProtectedMember() {
// 子類別可以存取從父類別繼承的 protected 成員
protectedMember = 42;
}
};
int main() {
Child child;
// 外部程式码無法直接存取子類別的 protected 成員
// child.protectedMember = 42; // 編譯錯誤
child.accessProtectedMember(); // 透過子類別的公共函數存取 protected 成員
return 0;
}
Child 類別使用
protected 繼承
Parent
類別繼承了protectedMember成員。
子類別中的 accessProtectedMember
函数可以直接存取
protectedMember,而外部程式碼無法直接存取子類別的
protected 成員。
公有繼承(public)
概念:子類別可以存取父類別的公有成員和保護成員,但無法直接存取私有成員。子類別對父類別的公有成員繼承保持相同的存取權限(即公有成員仍為公有,保護成員仍為保護)。
常見場景:這是『最常見』的繼承方式,通常用於「是種關係」的情況,即子類別是父類別的一種類型。比如,Dog
是 Animal 的一種,這樣的繼承關係一般使用公有繼承。
私有繼承(private)
概念:子類別對父類別的公有和保護成員進行私有化,這意味著子類別中的父類別成員無法被外部程式碼直接存取(即使是子類的實例也無法直接存取)。
常見場景:私有繼承通常用於需要使用父類別的功能或實現某些接口,但不希望讓外部程式碼直接與父類別互動的情況。例如,當你希望在子類別內部使用父類別的一些實現(例如,方法或資料),但不希望讓外部程式碼直接存取這些父類別的成員時,會選擇私有繼承。
保護繼承(protected)
概念:子類別對父類別的公有和保護成員進行保護化,這意味著子類別的成員可以存取父類別的公有和保護成員,但這些成員對外部程式碼(包括子類的實例)來說是不可見的。這種繼承方式通常用來強調「是種關係」,但同時限制了外部對父類別成員的存取。
常見場景:保護繼承適用於那些子類需要使用父類別成員,但同時希望限制外部對這些成員的存取的情況。例如,在繼承一個基礎類別時,子類別需要繼承並使用父類別的部分方法或資料,但不希望外部能夠直接修改這些成員。
子類別可以『覆寫(override)』父類別的
public 和
protected
函數,並可以更改函數的存取等級,但必須符合覆寫的規則。
函數覆寫(override):
子類別可以使用與父類別相同的函數名稱和參數來覆寫父類別的函數。這樣一來,當透過子類別的物件呼叫該函數時,會執行子類別的版本而非父類別的版本。
條件:
父類別的成員函數是『虛函數(virtual)』。
名稱、參數列表、回傳型態(需相容)都要完全相同。
在子類別中實作時,代表『重新定義父類別的行為』。
規則:
函數『名稱』、參數的『型別』及『順序』、『回傳型別』都必須完全相同,否則『不會』被認為是覆寫。
要達到「覆寫(override)」效果,父類別的成員函數必須是
virtual。否則即使子類別定義了同名、同參數、同回傳型態的函數,C++
也只會視為「遮蔽(hiding)」,而不是覆寫。
當父類別中的函數被標記為 virtual,則覆寫時建議使用
override
關鍵字以表明覆寫的意圖,則編譯器可進行檢查,避免覆寫錯誤。
override 是 C++11
引入的『編譯期檢查』關鍵字。若父類別沒有
virtual,但你用了
override,會直接編譯錯誤。
加上 override
可強制編譯器檢查「是否真的覆寫」父類別虛函數,避免意外遮蔽。
如果參數或回傳型態有任何差異,C++ 不會認為這是覆寫,而可能視為『函數遮蔽(Function Hiding)』。
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() { cout << "Base::show()\n"; }
};
class Derived : public Base {
public:
void show() override { cout << "Derived::show()\n"; } // 覆寫成功
};
int main() {
Base* b = new Derived();
b->show(); // 輸出:Derived::show()
}一般情況下,父類別的成員函數只要是用 public 或
protected 修飾的,子類別只要不使用 private
繼承父類別,則子類別都可以使用。
C++ 繼承中,子類別會『遮蔽(Hiding)父類別中的『同名函數』(不管函數的回傳值與參數)。
名稱相同,但
參數個數或型別不同
或回傳型態不同但不相容
或父函數不是 virtual
#include <iostream>
using namespace std;
class Base {
public:
virtual void show() { cout << "Base::show()\n"; }
virtual void show(int x) { cout << "Base::show(int)\n"; }
};
class Derived : public Base {
public:
void show(double x) { cout << "Derived::show(double)\n"; } // ⚠️ 遮蔽
};
int main() {
Derived d;
d.show(1.0); // 呼叫 Derived::show(double)
// d.show(1); // 編譯錯誤,因為 Base::show(int) 被遮蔽掉了
d.Base::show(1); // 可以用 Base:: 重新指定
}
// Human.h
#ifndef __Human__
#define __Human__
class Human
{
public:
Human();
Human(int);
public:
int m_Age;
char m_Name[100];
protected:
int m_prol;
void funcpro() {};
private:
int m_privl;
void funcpriv();
public:
void samenamefunc();
void samenamefunc(int);
};
#endif /* __Human__ */
// Human.cpp
#include "Human.h"
#include <iostream>
Human::Human()
{
std::cout << "執行Human::Human()建構函數" << std::endl;
}
Human::Human(int age)
{
std::cout << "執行Human::Human(int age)建構函數" << std::endl;
}
void Human::samenamefunc()
{
std::cout << "執行Human::samenamefunc()建構函數" << std::endl;
}
void Human::samenamefunc(int)
{
std::cout << "執行Human::samenamefunc(int)建構函數" << std::endl;
}
// Men.h
#ifndef __Men__
#define __Men__
#include "Human.h"
class Men:protected Human
{
public:
Men();
public:
void samenamefunc(int);
};
#endif /* __Men__ */
// Men.cpp
#include "Men.h"
#include <iostream>
Men::Men()
{
std::cout << "執行Men::Men()建構函數" << std::endl;
}
void Men::samenamefunc(int)
{
std::cout << "執行Men::samenamefunc(int)" << std::endl;
}
Men men;
// men.samenamefunc(); // 錯誤:無法呼叫父類別不帶參數的samenamefunc函數
men.samenamefunc(1); // 正確:只能呼叫子類別具一個參數的samenamefunc函數,無法呼叫父類別帶一個參數的samenamefunc函數。
// 執行Human::Human()建構函數
// 執行Men::Men()建構函數
// 執行Men::samenamefunc(int)若欲呼叫父類別的同名函數呢?可借助子類別的成員
samenamefunc 函數來呼叫父類別的成員函數
samenamefunc 函數。
void Men::samenamefunc(int)
{
Human::samenamefunc(); // 可以使用父類別的無參數的smaenamefunc函數。
Human::samenamefunc(120); // 可以使用父類別的帶一個參數的smaenamefunc函數。
std::cout << "執行void Men::samenamefunc(int)" << std::endl;
}
Men men;
// men.samenamefunc();
men.samenamefunc(1);
// 執行Human::Human()建構函數
// 執行Men::Men()建構函數
// 執行Human::samenamefunc()建構函數
// 執行Human::samenamefunc(int)建構函數
// 執行void Men::samenamefunc(int)若子類別 Men 是以 public 繼承方式繼承
Human 父類別,則可在 main
主函數中使用:子類別物件名.父類別名::成員函數名(…):
men.Human::samenamefunc(); // 呼叫父類別中不帶參數的samenamefunc函數。
men.Human::samenamefunc(160); // 呼叫父類別中帶一個參數的samenamefunc函數。函數覆寫 (Function Overriding) 與 函數遮蔽 (Function Hiding) 的差異:
| 函數覆寫 (Function Overriding) | 函數遮蔽 (Function Hiding) | |
|---|---|---|
| 發生情境 | 子類別定義了與父類別完全相同簽名(名稱、參數、回傳值)的虛函數。 | 子類別定義了與父類別名稱相同但參數、回傳值可能不同的函數。 |
| 關鍵字要求 | 父類別函數需要使用 virtual
關鍵字,並在子類別中可選擇使用 override
明確表明覆寫行為(C++11 以上)。 |
不需要任何關鍵字,僅需在子類別中定義相同名稱的函數即可。 |
| 影響範圍 | 覆寫的是父類別的虛函數,透過多態機制實現動態綁定。 | 子類別中具有相同名稱的函數會隱藏父類別中所有名稱相同的函數。 |
| 是否動態綁定 | 是,透過虛函數表(Virtual Table)『動態綁定』。 | 否,函數遮蔽是『靜態綁定』,完全取決於編譯時的類型資訊。 |
| 是否受參數影響 | 子類別函數簽名(名稱、參數、回傳值)需完全匹配父類別虛函數。 | 子類別函數名稱相同即可,與參數和回傳值無關。 |
| 行為影響 | 覆寫後,當透過『父類別』指標或參考呼叫函數時,執行的是子類別版本的函數。 | 遮蔽後,子類別中的同名函數會隱藏父類別的所有同名函數,導致父類別的函數無法直接被子類別使用。 |
| 使用情境 | 用於實現『多態』行為,讓子類別提供自己的實現來取代父類別的實現。 | 子類別定義的函數完全取代父類別的函數,或需要不同功能的實現。 |
覆寫:強調「同一介面,多種實作」,適合框架、抽象類別、多態。依據物件「真實型別」決定執行哪個版本(動態繫結/綁定)
遮蔽:強調「重新定義或額外重載」,適合提供新介面或額外功能,不需動態綁定。編譯期依據「指標/變數型別」決定呼叫哪個版本(靜態繫結/綁定)
覆寫(override):父類別給你規格,子類別提供不同實作。
遮蔽(hiding):子類別自訂新版本,不理會父類別舊規格。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void foo() { cout << "Base::foo()\n"; }
virtual void bar(int x) { cout << "Base::bar(int)\n"; }
};
class Derived : public Base
{
public:
void foo() override { cout << "Derived::foo()\n"; } // 覆寫 (名稱與簽名完全相同)
void bar(double x) { cout << "Derived::bar(double)\n"; } // 遮蔽 (名稱相同但簽名不同)
};
int main()
{
Derived d;
Base* bp = &d;
bp->foo(); // Derived::foo() (覆寫成功,動態綁定)
bp->bar(10); // Base::bar(int) (父類別版本被呼叫)
d.bar(10.5); // Derived::bar(double)
// d.bar(10); // 編譯錯誤:Base::bar(int) 被遮蔽
}
foo():
完全符合簽名 + 父類別是 virtual → 覆寫。
透過 Base* 呼叫時,執行子類別版本。
bar():
子類別提供 double 參數版本,與父類別
int 參數不同 → 遮蔽。
父類別的 bar(int) 被隱藏,d.bar(10)
會編譯錯誤,必須 d.Base::bar(10) 才能呼叫。
範例比較:有無 virtual 差異
父類沒有 virtual → 不是覆寫
#include <iostream>
using namespace std;
class Base {
public:
void show() { cout << "Base::show()\n"; }
};
class Derived : public Base {
public:
void show() { cout << "Derived::show()\n"; } // 只是遮蔽,不是覆寫
};
int main() {
Base* b = new Derived();
b->show(); // 輸出:Base::show(),因為靜態繫結
}
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 指標並讓它指向
Dog 或 Cat
的物件時,呼叫 speak
函數會根據物件的實際型態(Dog 或
Cat)來決定調用哪個
speak 的實現。Animal* a = new Dog();
a->speak(); // 輸出 "Bark"
Animal* b = new Cat();
b->speak(); // 輸出 "Meow"純虛函數:如果虛函數在父類別中沒有被實作,並且期望在子類別中被實作出來,則稱為『純虛函數』。純虛函數是用
= 0 語法宣告的。
virtual void pureVirtualFunction() = 0;『抽象類別』是一種包含一個或多個『純虛函數』的類別。這些純虛函數在抽象類別中沒有實作,只提供了『介面』,並要求子類別必須實作出這些函數。
抽象類別不能直接『實例化』,只能作為基礎類別,並透過子類別來實現具體功能。
QuantLib::Instrument
是一個『抽象類別』,包含多個『純虛函數』。以下是 Instrument
類別的一部分定義:
namespace QuantLib {
class Instrument {
public:
virtual bool isExpired() const = 0;
virtual void setupArguments(PricingEngine::arguments*) const = 0;
virtual void fetchResults(const PricingEngine::results*) const = 0;
virtual void setupExpired() const = 0;
virtual double NPV() const = 0; // 純虛函數,用於計算淨現值
};
}要使用
QuantLib::Instrument,我們需要定義一個子類別並實作所有純虛函數。
// MyBond.hpp
#include <ql/instrument.hpp>
class MyBond : public QuantLib::Instrument {
public:
MyBond(double faceValue, double couponRate, int yearsToMaturity);
bool isExpired() const override;
void setupArguments(QuantLib::PricingEngine::arguments* args) const override;
void fetchResults(const QuantLib::PricingEngine::results* results) const override;
void setupExpired() const override;
double NPV() const override;
private:
double faceValue_;
double couponRate_;
int yearsToMaturity_;
};
// MyBond.cpp
#include "MyBond.hpp"
MyBond::MyBond(double faceValue, double couponRate, int yearsToMaturity)
: faceValue_(faceValue), couponRate_(couponRate), yearsToMaturity_(yearsToMaturity) {}
bool MyBond::isExpired() const {
// 假設債券到期日期已過
return yearsToMaturity_ <= 0;
}
void MyBond::setupArguments(QuantLib::PricingEngine::arguments* args) const {
// 設定評價引擎所需的參數
}
void MyBond::fetchResults(const QuantLib::PricingEngine::results* results) const {
// 從評價引擎得到結果
}
void MyBond::setupExpired() const {
// 設置到期時的行為
}
double MyBond::NPV() const {
// 計算債券的淨現值
double npv = 0.0;
for (int i = 1; i <= yearsToMaturity_; ++i) {
npv += (faceValue_ * couponRate_) / std::pow(1.05, i); // 假設折現率為5%
}
npv += faceValue_ / std::pow(1.05, yearsToMaturity_);
return npv;
}抽象類別的功能:
設計靈活的架構:抽象類別提供了一個介面,使得具體實現可以獨立於抽象介面進行變化,從而實現靈活且可擴展的架構。
強制實現特定行為:派生類別必須實現所有純虛函數,這保證了所有派生類別都具備抽象類別所定義的基本行為。
虛函數覆寫(overriding):在子類別中重新定義父類別的虛函數稱為覆寫。C++11
引入了 override
關鍵字來明確指示函數覆寫了父類別的虛函數。
使用 override
關鍵字不是強制性的,其被用來覆寫父類別的虛函數。
在 C++ 中,即使不使用 override
關鍵字,子類別也可以覆寫父類別的虛函數,只要子類別中的函數具有與父類別中的虛函數相同的簽名(即相同的函數名、回傳類型和參數列表)。
但 override 關鍵字在現代 C++
程式設計中是非常有用的,因為它提供了額外的『型態檢查』。
當在子類別中的函數宣告後使用
override
關鍵字時,如果該函數無法有效覆寫父類別中的任何虛函數(例如,由於簽名不匹配),編譯器將拋出錯誤。這有助於在開發過程中及早發現錯誤,從而減少了誤解和潛在的錯誤。
class Base {
public:
virtual void someFunction();
};
class Derived : public Base {
public:
void someFunction() override; // 正確地覆蓋基礎類別的虛函數
void anotherFunction() override; // 錯誤:Base 沒有這個虛函數
};
在這個例子中,如果 Base
類別沒有名為 anotherFunction
的虛函數,則在編譯時 Derived 類的
anotherFunction 會因使用了
override 關鍵字而產生編譯錯誤。
故 override
關鍵字雖然為可選的,但建議使用,因其可增加程式碼的清晰度和安全性。
在 C++ 中,如果一個類別被設計為父類別並且其解構函數 『不是』
虛函數(virtual),則當透過父類別指標刪除子類別物件時,子類別的解構函數不會被調用,進而導致資源洩漏。
這是因為非虛解構函數僅根據指標的型別(而非物件的實際類型)來決定執行哪個解構函數。
問題說明(非虛解構函數導致的資源洩漏):
#include <iostream>
using namespace std;
class Base {
public:
~Base() {
cout << "Base destructor" << endl;
}
};
class Derived : public Base {
private:
int* data;
public:
Derived() {
data = new int[10]; // 分配動態記憶體
cout << "Derived constructor: Memory allocated" << endl;
}
~Derived() {
delete[] data; // 釋放動態記憶體
cout << "Derived destructor: Memory deallocated" << endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 只調用 Base 的解構函數
return 0;
}
// Base destructor
改良版:
任何作為父類別的類別都應有虛解構函數:
純虛解構函數:
= 0)』,但仍需提供其實現。先呼叫子類別的解構函數,再呼叫父類別的解構函數。
#include <iostream>
using namespace std;
class Base {
public:
virtual ~Base() = 0; // 純虛解構函數
};
Base::~Base() {
cout << "Base destructor" << endl;
}
class Derived : public Base {
public:
~Derived() {
cout << "Derived destructor" << endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 正確調用所有解構函數
return 0;
}
在 QuantLib 內,幾乎所有金融工具的評價、現金流生成、殖利率曲線計算 都透過「虛函數覆寫」實作多態。
父類別通常是抽象基底類 (abstract base
class),定義一組統一的介面(例如 Bond,
Instrument, PricingEngine,
TermStructure),而子類別根據不同模型或產品「覆寫」這些虛函數以提供具體實作。
Instrument → Bond →
FixedRateBond
(檔案:ql/instrument.hpp,
ql/instruments/bond.hpp)
// Instrument.hpp
class Instrument : public Observable, public Observer {
public:
virtual ~Instrument() = default;
// 虛函數:所有金融工具都要實作自己的淨現值計算方式
virtual void setupArguments(PricingEngine::arguments*) const = 0;
virtual void fetchResults(const PricingEngine::results*) const = 0;
virtual bool isExpired() const = 0;
Real NPV() const { return NPV_; } // 非虛,通用介面
protected:
mutable Real NPV_;
};
// Bond.hpp
class Bond : public Instrument {
public:
void setupArguments(PricingEngine::arguments* args) const override;
bool isExpired() const override;
};
// FixedRateBond.hpp
class FixedRateBond : public Bond {
public:
void setupArguments(PricingEngine::arguments* args) const override;
};
Instrument 定義了『抽象介面』
setupArguments()、fetchResults()、isExpired()。
Bond 覆寫
isExpired(),提供通用債券邏輯。
FixedRateBond 再覆寫
setupArguments(),加入固定利率債的特有欄位。
這種架構讓 QuantLib 的引擎 (BondEngine,
DiscountingBondEngine,
BlackBondEngine)可以用同樣的呼叫方式
(bond->NPV()) 來對不同債券類別運作。
YieldTermStructure → FlatForward,
ZeroCurve, InterpolatedDiscountCurve
(檔案:ql/termstructures/yieldtermstructure.hpp)
class YieldTermStructure : public TermStructure {
public:
virtual Rate zeroRate(const Date& d, const DayCounter&, Compounding, Frequency, bool extrapolate = false) const;
virtual DiscountFactor discount(const Date& d, bool extrapolate = false) const = 0;
};
// ql/termstructures/yield/flatforward.hpp
class FlatForward : public YieldTermStructure {
public:
DiscountFactor discount(const Date& d, bool extrapolate = false) const override;
};
YieldTermStructure
提供抽象的「折現曲線」介面。
子類別 FlatForward 覆寫
discount(),使用固定遠期利率計算。
這樣在評價器中只要持有
shared_ptr<YieldTermStructure>,就能動態使用不同曲線模型,達成『多態性』。
class PricingEngine : public Observable {
public:
struct results { virtual ~results() = default; };
struct arguments { virtual ~arguments() = default; };
virtual void calculate() const = 0;
};
// 子類別
class DiscountingBondEngine : public PricingEngine {
public:
void calculate() const override;
};
所有評價引擎都透過 calculate()
虛函數統一呼叫。
根據實際傳入的引擎(Black-Scholes、Hull-White、Discounting…),會自動切換邏輯。
QuantLib 使用這種覆寫架構讓新產品/模型能直接 plug-in。
EX:策略模型:把「演算法」抽離成可以替換的物件,在執行時動態切換
定義一個 抽象的定價引擎
PricingEngine。
實作一個 具體的
DiscountingBondEngine,用折現法算債券價格。
定義一個 Bond
類別,裡面不自己算價格,而是呼叫外面塞進來的引擎。
main() 裡組一個 3 年期債券,指定折現率,算出
NPV。
calculate() 是唯一的策略接口(Strategy
Interface)
純虛 → 強迫所有策略都要實作自己的演算法
每一個引擎都是一個獨立演算法模組
QuantLib 整個架構就是靠 Strategy Pattern 撐起來的。
#include <iostream>
#include <vector>
#include <memory>
#include <cmath>
// Strategy Pattern(策略模式):把「演算法」抽離成可以替換的物件,在執行時動態切換。
// Bond 不自己算價格,而是把計算外包給「可以更換的引擎」。
// Bond 都不用改,但可以任意切換引擎。
// 之後可以換成:
// DurationBondEngine
// MonteCarloBondEngine
// CurveFittingBondEngine
// MonteCarloBondEngine
// ================================
// 1. Observable 基類(簡化版)
// 目前這個 Observable 裡 什麼都沒有,只有一個虛擬 destructor。
// 在「正式的 QuantLib」裡,Observable 會負責維護
// observer清單、通知變動。但這邊只保留接口,讓你看到「引擎是可以被觀察的」這個概念。
// ================================
class Observable
{
public:
virtual ~Observable() = default;
};
// ================================
// 2. PricingEngine(抽象引擎)抽象基底類別
// ================================
// PricingEngine 繼承 Observable → 表示它是「可以被觀察的物件」。
// 在完整版本裡,Instrument 會註冊成 observer,一旦市場資料改變,engine 或 curve 改變,就會通知。
class PricingEngine : public Observable
{
public:
// 這兩個是 引擎的輸入 / 輸出資料結構的「接口」。
// 基底的 arguments / results 裡面沒有欄位,只提供虛擬 destructor。
// 用意是:
// 各種不同產品的引擎,都可以 繼承 這兩個 struct,自訂欄位:
// 債券:到期日、票面金額、現金流陣列、貼現率 …
// 選擇權:標的價格、波動度、strike、到期日 …
// 因為 destructor 是 virtual,以後可以用 base pointer 安全 delete。
struct arguments
{
virtual ~arguments() = default;
};
struct results
{
virtual ~results() = default;
};
// 純虛函式
// 這一行讓 PricingEngine 變成 抽象類別:
// 不能 new PricingEngine。
// 一定要由 子類別 override 這個函式。
// 之後所有具體的引擎(例如 DiscountingBondEngine)都會實作 calculate(),在裡面用 arguments 算出 results。
virtual void calculate() const = 0;
// 成員指標
// 為了支援多型:真實使用時,這兩個指標會指向 衍生類別,例如:
// DiscountingBondEngine::arguments
// DiscountingBondEngine::results
// mutable: 需要在 calculate() 裡 讀寫 arguments_ / results_ 指向的東西。
mutable arguments* arguments_ = nullptr;
mutable results* results_ = nullptr;
};
// ================================
// 3. CashFlow 結構
// ================================
struct CashFlow
{
double amount;
double time; // 以「年」為單位的時間
CashFlow(double amt, double t) : amount(amt), time(t) {}
};
// ================================
// 4. DiscountingBondEngine(子引擎)
// 表示這個類別是一個 具體的定價引擎,可拿來算價格。
// 它要滿足:
// 實作 calculate() const
// 定義自己專用的 arguments 和 results(繼承自基底)。
// ================================
class DiscountingBondEngine : public PricingEngine
{
public:
// 這個 struct 是 專門給這個引擎使用的輸入格式。
// cashflows:所有的現金流(每一年利息、本金)。
// discountRate:貼現率 r(假設是固定年利率,不是整條曲線)。
// 在 QuantLib 裡,這通常會放:std::vector<boost::shared_ptr<CashFlow>>、Handle<YieldTermStructure> discountCurve; 等。
struct arguments : PricingEngine::arguments
{
std::vector<CashFlow> cashflows;
double discountRate; // 固定年利率 r
};
// 把計算結果(債券價格)放在 npv 欄位。
struct results : PricingEngine::results
{
double npv = 0.0;
};
void calculate() const override
{
// arguments_ 型別是 PricingEngine::arguments*,但實際上指向的是 DiscountingBondEngine::arguments。
// 用 dynamic_cast 轉回真實型別,方便存取欄位 cashflows、discountRate。
// results_ 同理,轉回 DiscountingBondEngine::results,才能寫 res->npv。
auto* args = dynamic_cast<const arguments*>(arguments_);
auto* res = dynamic_cast<results*>(results_);
// 折現公式
double total = 0.0;
for (const auto& cf : args->cashflows)
{
double df = std::exp(-args->discountRate * cf.time);
total += cf.amount * df;
}
res->npv = total;
}
};
// ================================
// 5. Bond instrument
// Bond 類別:Instrument,負責持有現金流與引擎
// 放現金流
// 將 arguments 傳給引擎
// 呼叫 engine_->calculate()
// ================================
class Bond
{
public:
// 傳入一組現金流,move 進 cashflows_,避免多餘複製。
// cashflows_ 是這支債券的所有現金流資訊。
Bond(std::vector<CashFlow> cfs) : cashflows_(std::move(cfs)) {}
// Bond 本身不管引擎是什麼,只知道「要有一個 PricingEngine」。
// 這實際上就是一種 策略模式(Strategy Pattern):
// 「債券」這個 context,把計算邏輯委託給外部注入的 strategy(引擎)。
void setPricingEngine(std::shared_ptr<PricingEngine> engine)
{
engine_ = engine;
}
double NPV(double discountRate) const
{
// 建立引擎用的輸入與輸出物件
auto* args = new DiscountingBondEngine::arguments();
auto* res = new DiscountingBondEngine::results();
// 把債券資料填進 arguments
args->cashflows = cashflows_;
args->discountRate = discountRate;
// 將 arguments / results 交給 engine
engine_->arguments_ = args;
engine_->results_ = res;
// 呼叫引擎計算
engine_->calculate();
double value = res->npv;
// 釋放記憶體 & 回傳結果
delete args;
delete res;
return value;
}
private:
// cashflows_:債券內部的現金流。
// engine_:指向一個外部提供的定價引擎。用 shared_ptr 是為了在多個 instrument 之間共享同一個引擎時比較方便。
std::vector<CashFlow> cashflows_;
std::shared_ptr<PricingEngine> engine_;
};
// ================================
// 6. 主程式
// ================================
int main()
{
std::cout << "=== Bond Pricing Engine Example ===\n";
// 建立 3 年期固定利率債券
// 每年支付 5 元,最後返還本金 100 元
std::vector<CashFlow> cfs = {
CashFlow(5.0, 1.0), CashFlow(5.0, 2.0), CashFlow(105.0, 3.0) // 100 本金 + 5 利息
};
Bond bond(cfs);
// 使用折現法引擎
auto engine = std::make_shared<DiscountingBondEngine>();
bond.setPricingEngine(engine);
double r = 0.03; // 3% discount rate
double price = bond.NPV(r);
std::cout << "Discount rate = " << r << "\n";
std::cout << "Bond price = " << price << "\n";
return 0;
}Bond 是不會算價格的;
是 PricingEngine 負責算價格;
而 PricingEngine 可以自由替換不同的演算法。
這就是 Strategy Pattern。
相較於 override,QuantLib 使用 hiding 的情況較少。
主要是為了「避免父類別虛函數干擾」或「提供重載版本」。
Date 類別中重載與遮蔽 (ql/time/date.hpp)
class Date {
public:
// 重載版本(不是虛函數)→ 根據輸入類型遮蔽其他同名版本
static Date minDate();
static Date maxDate();
Integer dayOfMonth() const;
Integer month() const;
Integer year() const;
// 隱藏掉其他同名重載的 toString()
std::string toString() const;
};
toString() 並沒有覆寫任何東西,只是「遮蔽」了
std:: 的同名函數。
QuantLib 明確希望自己的 Date::toString() 不受
std::to_string() 影響。
Observer / Observable 的重載遮蔽
(ql/patterns/observable.hpp)
class Observer {
public:
virtual void update() = 0;
};
class Observable {
public:
void notifyObservers() const;
void notifyObservers(const void* tag) const; // 同名不同參數 → 函數遮蔽
};
notifyObservers() 有兩個重載版本。
它們不是虛函數,因此不是覆寫,而是同名遮蔽(overload hiding)。
這裡的設計用意是提供「一般通知」與「帶識別碼通知」兩種版本。
QuantLib 透過 override 建立「金融產品與模型的多態架構」,而 hiding 僅用於「重載介面設計」與「避免父類別干擾」。
| 概念 | 發生範圍 | 是否需要 virtual | 決定時機 | 多態行為 | QuantLib 是否常用 |
|---|---|---|---|---|---|
| Overloading (多載) | 同一類別內 | 否 | 編譯期 | 無 | 常見 |
| Override (覆寫) | 父類別 ↔︎ 子類別 | 要有 virtual |
執行期 | 有 | 非常常見 |
| Hiding (遮蔽) | 父類別 ↔︎ 子類別 | 否 | 編譯期 | 無 | 偶爾出現 |
friend Function)C++ 中的友函數(Friend
Function)是一種特殊的函數,它被允許存取一個類別的 private
和 protected 成員。
友函數不是類別的成員函數,但它被指定為該類別的『朋友(Friend)』。
宣告 friend 並不代表「這個函數屬於類別」;
把這個函數列入「可以存取我的 private/protected」的白名單。
不改變函數的作用域(仍是全域函數)。
特點:
存取權限:友函數可以存取該類的『所有成員』,包括private
和 protected成員。
非成員函數:友函數不是類別的成員函數,因此它不是由物件來調用的。它就像一般的函數一樣,但有存取特定類別的private和protected成員的能力。
宣告方式:在『類別的定義內』使用 friend
關鍵字來宣告友函數。
互不影響:友函數的宣告不影響類別的封裝特性。類別的實現細節仍然隱藏於使用者。
作用範圍:友函數的作用範圍不是類別的範圍,而是與它被定義的區域相同。
友函數可以是全局函數,也可以是另一個類別的成員函數。
友函數通常用於當函數需要存取多個類別的私有成員時。例如,重載某些運算子(如
<< 或
>>
用於輸出/輸入)時,可能需要存取類別的私有成員變數。
class MyClass {
private:
int x;
public:
MyClass(int val) : x(val) {}
// 宣告友函數
friend void showX(MyClass&);
};
// 定義友函數
void showX(MyClass& m) {
// 直接存取私有成員 x
std::cout << "MyClass::x = " << m.x << std::endl;
}
int main() {
MyClass obj(10);
showX(obj); // 輸出 MyClass::x = 10
return 0;
}
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}
// 友函數
friend void showValue(MyClass&);
};
// 全局範圍內定義的友函數
void showValue(MyClass& m) {
std::cout << "Value: " << m.value << std::endl;
}
int main() {
MyClass obj(100);
showValue(obj); // 在全局範圍內調用
return 0;
}
showValue
是一個全局函數,它被宣告為 MyClass
的友函數,因此能夠存取 MyClass 的私有成員
value。儘管
showValue
被宣告為友函數,但它仍然是一個全局函數,可以在全局範圍內被調用。class Point {
int x_, y_;
public:
Point(int x, int y): x_(x), y_(y) {}
friend std::ostream& operator<<(std::ostream&, const Point&);
};
std::ostream& operator<<(std::ostream& os, const Point& p) {
return os << "(" << p.x_ << "," << p.y_ << ")"; // OK
}
// 非成員(友函數)版本
class Vector {
double x_, y_;
public:
Vector(double x, double y) : x_(x), y_(y) {}
// 非成員函數(友元)
friend Vector operator+(const Vector& lhs, const Vector& rhs) {
return Vector(lhs.x_ + rhs.x_, lhs.y_ + rhs.y_);
}
};
int main() {
Vector a(1, 2), b(3, 4);
Vector c = a + b; // 等同於 operator+(a, b)
}
// 成員函數版本
class Vector {
double x_, y_;
public:
Vector(double x, double y) : x_(x), y_(y) {}
// 成員函數版本:左邊是自己
Vector operator+(const Vector& rhs) const {
return Vector(x_ + rhs.x_, y_ + rhs.y_);
}
};
int main() {
Vector a(1, 2), b(3, 4);
Vector c = a + b; // 等同於 a.operator+(b)
}所有函數(成員或非成員)都在程式的「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)是一種允許某個類別存取另一個類別的私有成員和保護成員的機制。
QuantLib 中 Handle<T>
可以存取某些曲線類的內部資料。
friend 只是「編譯期」的權限設定。
告訴編譯器:B 類別可以存取 A 類別的 private / protected 成員。
所以記憶體上:
A 物件仍然是一塊固定大小的記憶體
B 物件也是自己的記憶體
兩者 沒有額外的指標或參考
沒有多做任何 new/delete
它是純語法層級的授權,不影響執行期記憶體。
friend 是強大的武器,但濫用會破壞封裝性。
通常,私有成員只能在類別的成員函數內部存取,而友類別可以越過這些限制,直接存取其他類別的私有成員和保護成員。
friend 關鍵字來指定哪些類是該類的友類別。class B; // 前向宣告
class A {
private:
int x;
public:
A() : x(10) {} // 建構子
friend class B; // B 是 A 的友類別
};
class B {
public:
void printA(const A& a) {
// B 類可以存取 A 類的私有成員
std::cout << "x = " << a.x << std::endl;
}
};
int main() {
A a;
B b;
b.printA(a); // 可以存取 A 類的私有成員
return 0;
}
A 類中,我們使用
friend class B; 來將 B 類標註為 A
類的友類別。B 類的
printA 函數中,我們可以直接存取 A 類的私有成員
x。這是因為 B 是 A 的友類別,即使
x 是私有成員,B 仍然可以存取它。class A {
friend class B; // B 是 A 的朋友
private:
int secret_ = 42;
};
class B {
public:
void showSecret(const A& a) {
std::cout << a.secret_ << std::endl; // ✅ 可以存取 private 成員
}
};
int main() {
A a;
B b;
b.showSecret(a); // 輸出 42
}B 是「A 的朋友」;
所以 B 裡的任何成員函數都可以看到 A
的內部資料;
但反過來不行(A 看不到 B 的
private)。
#include <iostream>
#include <string>
class Manager; // 前向宣告
class Employee {
private:
std::string name;
double salary;
public:
Employee(const std::string& n, double s) : name(n), salary(s) {}
// 宣告 Manager 為友類別
friend class Manager;
void displayInfo() const {
std::cout << "Employee: " << name << ", Salary: " << salary << std::endl;
}
};
class Manager {
public:
void giveRaise(Employee& e, double amount) {
// 由於 Manager 是 Employee 的友類別,所以可以直接修改 Employee 的私有成員
e.salary += amount;
std::cout << "Salary after raise: " << e.salary << std::endl;
}
void displayEmployeeInfo(const Employee& e) {
// 也可以存取 Employee 的私有成員
std::cout << "Manager viewing: " << e.name << "'s salary: " << e.salary << std::endl;
}
};
int main() {
Employee emp1("John", 50000);
Manager mgr;
// 顯示員工資訊
emp1.displayInfo();
// 管理員查看員工的私有成員
mgr.displayEmployeeInfo(emp1);
// 管理員給員工加薪
mgr.giveRaise(emp1, 5000);
return 0;
}
Employee
類:這個類有兩個私有成員,name 和
salary,以及一個公共方法 displayInfo()
用來顯示員工的基本信息。Manager 類:這個類有兩個方法:
giveRaise():該方法可以給員工加薪,這是因為
Manager 是 Employee
類的友類別,可以直接存取員工的私有成員 salary。
displayEmployeeInfo():這個方法允許
Manager 查看員工的私有資訊(如薪水)。
Employee 類中,使用
friend class Manager; 將 Manager
類設為友類別。這樣,Manager 類的成員函數就能夠存取
Employee 類的私有成員。class Product {
friend class ProductBuilder; // 授權 builder
private:
std::string name_;
double price_ = 0.0;
};
class ProductBuilder {
Product p_;
public:
ProductBuilder& setName(std::string n) { p_.name_ = std::move(n); return *this; }
ProductBuilder& setPrice(double pr) { p_.price_ = pr; return *this; }
Product build() { return p_; }
};
ProductBuilder 可以直接設定 Product 的
private 成員,但外部仍然無法改動,確保封裝性。friend class 是「整體授權版本的 friend
函數」。
它不會改變記憶體結構,不會讓兩個物件共享空間,只是在編譯階段讓編譯器放行:「B 可以看 A 的內部」。
在 QuantLib
裡,「友類別(friend class)」的應用非常典型,最著名的例子有
Handle<T> 與
RelinkableHandle<T>。另外,QuantLib 利用
friend 讓 Handle/Observer 可以操作
observers_
這組類別的設計非常優雅,展示了「友類別」如何在不破壞封裝的前提下允許另一類直接操作內部狀態。
Handle<T> 需要讀寫
被觀察物件(Observable) 的私有狀態
Observable 也需要管理 誰正在觀察它(observer list)
Handle<YieldTermStructure>
會直接操作曲線的私有狀態)
TermStructure 的 bootstrapper 需要直接存取曲線的
private 欄位(例如 discount vector、nodes、dates)
Instrument 把 arguments &
results 託付給 Engine,使用 friend 授權 Engine
寫入。
多重繼承允許一個類別同時繼承自多個父類別。
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成員存取順序:
菱形繼承問題:
多重繼承可能引發菱形繼承问题:意指子類別繼承了多個類別,而該類別最终都繼承自同一個基礎類別。這可能導致二義性,因為子類別不知道應該使用哪個基礎類別的成员。
父類別的多次建構:最終子類別會多次建構同一個父類別的物件。
成員或方法的二義性:子類別無法區分應該呼叫哪一個父類別中的方法或成員。
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
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;
}
考慮以下情境:
VirtualDerived1 繼承自
Base。
VirtualDerived2 繼承自
Base。
DiamondDerived 同時繼承自
VirtualDerived1 和
VirtualDerived2。
若不使用虛擬繼承,則 DiamondDerived
會在繼承過程中擁有兩個 Base
子物件,一個來自 VirtualDerived1,一個來自
VirtualDerived2。這可能導致二義性,因為編譯器不知道應該使用哪個
Base 子物件的成員。
但使用虛擬繼承後,DiamondDerived
只擁有一個共同的 Base 子物件,它來自於
VirtualDerived1 和
VirtualDerived2 中的
Base。這樣就解決了二義性問題,因為只有一個
Base 子物件可供存取。
虛擬繼承確保了在多重繼承中共享的基礎類別只有一個『實例』,從而解決了二義性問題。
#include <iostream>
using namespace std;
class A {
public:
A() { cout << "Constructing A" << endl; }
void show() { cout << "A::show" << endl; }
};
class B : virtual public A {
public:
B() { cout << "Constructing B" << endl; }
};
class C : virtual public A {
public:
C() { cout << "Constructing C" << endl; }
};
class D : public B, public C {
public:
D() { cout << "Constructing D" << endl; }
};
int main() {
D obj;
obj.show(); // 不再二義性
return 0;
}多態:行為上的概念,著重於如何透過統一的介面(如父類別指標或參考)來操作多個子類別物件。
多態:同一個操作作用在不同的物件上,將產生不同的執行結果。
多重繼承:結構上的概念,讓類別可以從多個來源繼承屬性和行為。
在多重繼承中,多態仍然適用。例如:
#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 模式 用於「保證某個類別在整個程式中只有一個實例」,並提供一個全域、受控的存取介面。
用於只能創建只能產生一個物件(實例)的的類別。
在 QuantLib,用來管理全局唯一的資源,如市場資料或設定。
實現細節:
核心類別:
Settings:管理全局設定。檔案位置:
ql/settings.hpp使用案例:
使用 Settings::instance()
取得全局設定物件,例如評價日期(evaluation date)。
瞭解 QuantLib 中 Settings
類別如何使用單例模式管理全局設定。
繼承關係:Settings : public Singleton<Settings>
避免多個設定實例造成混亂
在 QuantLib 中,可透過 Settings::instance()
取得單例物件,進而操作上述設定。
Settings
類別用來管理執行時的全局設定,主要功能包括:
評價日期 (Evaluation Date): 控制計算時的基準日期,允許設置、讀取和監控其變化。
參考日期事件 (Reference Date Events): 決定是否將發生在參考日期的事件視為已發生。
今日現金流量 (Today’s Cash Flows): 指定是否將當天的現金流量計入 NPV。
歷史修正值 (Historical Fixings): 設定是否強制應用當天的歷史修正值。
單例(Singleton) 範例 1:簡化版單例(GameConfig)
#include <iostream> // 引入標準輸入輸出庫
// 定義 GameConfig 類別,實現單例模式
class GameConfig
{
private:
// 私有的構造函數,防止外部直接建立物件
GameConfig()
{
std::cout << "GameConfig created!" << std::endl;
}
// 禁止使用複製構造函數
GameConfig(const GameConfig &) = delete;
// 禁止使用賦值運算符
GameConfig &operator=(const GameConfig &) = delete;
// 私有的解構函數,確保靜態變數在程式結束時釋放
~GameConfig()
{
std::cout << "GameConfig destroyed!" << std::endl;
}
public:
// 提供唯一的全域存取點,回傳單例實例
static GameConfig &getInstance()
{
// 靜態局部變數,只有第一次呼叫時初始化,之後都回傳同一個實例
static GameConfig instance; // 這一行位於 getInstance() 函數內,代表的是 函數內的靜態區域變數。
return instance;
}
// 公有成員函數,顯示單例實例的記憶體地址
void showMessage() const
{
std::cout << "GameConfig instance address: " << this << std::endl;
}
};
int main()
{
// 取得單例實例的第一個引用,並呼叫 showMessage() 確認地址
GameConfig &config1 = GameConfig::getInstance();
config1.showMessage(); // 預期輸出: GameConfig instance address: [實例地址]
// 取得單例實例的第二個引用,並再次呼叫 showMessage() 確認地址
GameConfig &config2 = GameConfig::getInstance();
config2.showMessage(); // 預期輸出與 config1 的地址相同
// 回傳 0,結束程式
return 0;
}
唯一性:限制類別只能有一個實例(透過私有建構子)。
控制存取:提供一個全域的存取點。
延遲初始化:只有在第一次需要時才會建立實例。
將構造函數設為私有,防止外部使用 new
或其他方式建立實例。
作用:限制實例的建立,確保只能透過 getInstance()
取得實例。
禁止複製與賦值
刪除複製構造函數與賦值運算子,防止透過複製或賦值生成新的實例。
作用:保證單例模式的唯一性。
private:
GameConfig() { ... }
GameConfig(const GameConfig &) = delete;
GameConfig &operator=(const GameConfig &) = delete;建構函數設為 private:不讓外部呼叫
new GameConfig(),也不能寫
GameConfig config;。靜態方法
getInstance()。
刪除拷貝建構子與賦值運算子:避免
GameConfig config2 = config1;
這種方式複製出第二個實例。
靜態局部變數:static GameConfig instance 保證
instance 只會被初始化一次。
記憶體管理:靜態局部變數在程式結束時自動釋放。
執行流程:
第一次呼叫 getInstance() 時,創建
instance。
後續呼叫直接回傳已經存在的 instance。
第一次呼叫 getInstance():
靜態變數 instance 被初始化。
輸出 "GameConfig created!"。
config1 取得 instance,呼叫
showMessage(),輸出實例記憶體位址。
第二次呼叫 getInstance():
靜態變數 instance 已經存在,直接回傳。
config2 與 config1 指向相同實例,呼叫
showMessage(),再次輸出相同記憶體位址。
程式結束:
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
在多執行緒中建立時也是安全的(只初始化一次)。