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
修飾符提供各種實現類別的功能,但不曝露給外部使用者存取。
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 "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
:這部分程式碼存取傳入物件
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___ */
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;
}
範例:
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):
子類別可以使用與父類別相同的函數名稱和參數來覆寫父類別的函數。這樣一來,當透過子類別的物件呼叫該函數時,會執行子類別的版本而非父類別的版本。
函數覆寫(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
成員。
一般情況下,父類別的成員函數只要是用 public
或
protected
修飾的,子類別只要不使用 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
指標並讓它指向
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;
}
friend
Function)C++ 中的友函數(Friend
Function)是一種特殊的函數,它被允許存取一個類別的 private
和 protected
成員。
友函數不是類別的成員函數,但它被指定為該類別的『朋友(Friend)』。
特點:
存取權限:友函數可以存取該類的『所有成員』,包括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
被宣告為友函數,但它仍然是一個全局函數,可以在全局範圍內被調用。多重繼承允許一個類別同時繼承自多個父類別。
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
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 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;
}
用於只能創建只能產生一個物件(實例)的的類別。
在 QuantLib,用來管理全局唯一的資源,如市場資料或設定。
實現細節:
核心類別:
Settings
:管理全局設定。檔案位置:
ql/settings.hpp
使用案例:
使用 Settings::instance()
取得全局設定物件,例如評價日期(evaluation date)。
瞭解 QuantLib 中 Settings
類別如何使用單例模式管理全局設定。
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()
:
靜態變數 instance
被初始化。
輸出 "GameConfig created!"
。
config1
取得 instance
,呼叫
showMessage()
,輸出實例記憶體位址。
第二次呼叫 getInstance()
:
靜態變數 instance
已經存在,直接回傳。
config2
與 config1
指向相同實例,呼叫
showMessage()
,再次輸出相同記憶體位址。
程式結束:
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;
}