C++ 多載介紹 (運算子多載&函數多載) | seansie blog
簡介
多載(overloading)這個概念有點像是 破音字 的概念,例如”吃”這個字,在”吃東西”這個語境下,念(音:痴),而在”口吃”這個字念(音 : 級),同一個字,可以根據上下文來決定不同意義。
而C++的多載也一樣,也就是同一個東西(例如函數、運算子等等),可能會根據上下文來決定不同的意義。
函數多載
為什麼要用這個?
想像一下,我們要實作一個相加(這只是為了舉例方便而已,實際上更難)的函數,而因為 C++有不同的 資料型態 ,所以每個資料型態都要分別定義自己的函數。
int addInt(int a, int b) {
return a + b;
}
double addDouble(double a, double b) {
return a + b;
}
float addFloat(float a, float b) {
return a + b;
}
string addString(string a, string b) {
return a + b;
}
然後要使用的時候,還要分別記住 addInt
、addDouble
、addFloat
和 addString
,不僅沒有效率,這樣要用的時候還容易出錯,且程式碼會變得冗長、難以維護。
int main() {
int intResult = addInt(5, 3); // 使用 addInt
double doubleResult = addDouble(2.5, 1.5); // 使用 addDouble
float floatResult = addFloat(1.0f, 2.0f); //使用addFloat
std::string stringResult = addString("Hello", " World"); // 使用 addString
// ... (使用這些結果) ...
return 0;
}
多載解決方案
其實在C++中,編譯器是允許相同名字的函數的,如同開頭的破音字譬喻一樣,名字一樣沒關係,只要有能分辨的上下文就可以。
那怎麼分辨呢? 用 **函數原型 ,**來分辨,如下所示。
// int version
int add(int a, int b) {
return a + b;
}
// double version
double add(double a, double b) {
return a + b;
}
// float version
float add(float a, float b) {
return a + b;
}
// string version (concatenation)
std::string add(const std::string& a, const std::string& b) {
return a + b;
}
在上面這個程式碼中,讓我們來看看每個函數的原型。
- 第一個
int add(int,int)
- 第二個
double add(double,double)
- 第三個
float add(float,float)
- 第四個
string add(string,string)
很明顯,每個原型都不一樣,而多載的上下文就是原型,因此這四個 同名 函數是可以合法的存在的。
而怎麼使用呢,如下所示
int main() {
std::cout << add(5, 3) << std::endl; // Output: 8 (int)
std::cout << add(2.5, 3.7) << std::endl; // Output: 6.2 (double)
std::cout << add("Hello", " World") << std::endl; // Output: Hello World (string)
return 0;
}
在這個例子中,第一行中 add(5,3)
,裡面的參數可以被識別出來是 (int,int)
的資料型態,因此能根據剛剛的上下文,推論出要執行這個int add(int,int)
對應的函數。
注意:
const int
跟int
在多載函數中的原型下,會被視為相同的!
預設參數
有時候,有些約定俗成的東西,一直重複真的很麻煩 (請完成這個句子) ,不如直接設定成預設參數,省時省力。
void greet(const std::string& name, const std::string& greeting = "Hello", const std::string& punctuation = "!") {
std::cout << greeting << ", " << name << punctuation << std::endl;
}
在這個打招呼的函數 greet
,大家打招呼除非另有要求,不然大概都是千篇一律的 "Hello"
吧,此外,結尾的標點符號,也通常是 !
,這些在上面的函數就可以設定成 預設參數
而設定的方法就是在函數定義中,在參數定義後面加上 std::string& greeting = "Hello"
,就是後面的 = "Hello"
。
如此一來,如果這個參數沒有填入,就會自動填入預設值,極為 "Hello"
。
int main() {
// 使用所有參數
greet("Alice", "Good morning", ".");
// 只提供名字,使用預設的問候語和標點符號
greet("Bob");
// 提供名字和問候語,使用預設的標點符號
greet("Charlie", "Hi");
//不好的做法,一直重複"How are you", "?"
greet("Dave", "How are you", "?");
greet("Eve", "How are you", "?");
return 0;
}
運算子多載
最常見的例子
在C+++中, string
這個物件應該非常常用吧,應該不少人已經用過了其運算子多載的功能。
#include <iostream>
#include <string>
int main() {
std::string s1 = "Hello, ";
std::string s2 = "world!";
std::string s3 = s1 + s2; // 使用 + 连接
std::cout << s3 << std::endl; // 输出: Hello, world!
return 0;
}
可是在C++運算子的介紹, +
理論上只能針對數字阿,字串怎麼能相加呢?
因此,其實字串根本沒有數學意義上的相加,而是呼叫了類似 strcat
的字串合併函數,並且多載到 +
運算子上,其上下文是該運算子的左邊跟右邊的運算元,這個例子下是 string+string
。
如此一來,如果當編譯器讀取到 foo+bar
,而確定 foo
跟 bar
都是 string
物件後,就會呼叫多載的對應函數,而對於 string
函數來說, 相加的意思就是把兩個字串相連接,因此會呼叫 連接(foo,bar)
,然後該函數會回傳相連接好的字串。
怎麼使用
這裡以分數運算的例子為例,然後因為篇幅考量,本次只實作輸入輸出、乘法與除法,因為比較簡單。
首先先定義分數的類別
class frac{
public:
frac(int a,int b){
this->nu=a;
this->de=b;
if (b == 0) {
throw std::invalid_argument("Denominator cannot be zero."); // 分母不可為0
}
}
int getNu(){
return nu;
}
int getDe(){
return de;
}
private:
int nu,de // nu=numerator (分子) de=demoninator (分母)
}
其中,按照慣例資料要封裝起來,因此 nu
跟 de
即分母與分子,要被放在 private
中,限制存取。
而分母跟分子還是要有介面可供外界存取,因此在 public
中提供個兩個函數(介面),即 getNu
與 getDe
。
然後還要有建構子來方便的初始化分數物件,順帶一題 this
關鍵字在建構子是個指標,指向欲建立的新物件,因此如果要透過 this
來存取其內部成員,會用 this->nu
裡面的 ->
箭頭運算子。
多載運算子語法
在類別定義內,如果要定義多載函數,語法如下
friend 回傳的物件 operator 要多載的運算子 (const 物件參考 , const 物件參考 ,,,,,,){函數本體}
friend
是C++
的關鍵字,是讓這個函數宣告既在類別定義中,而又可以存取類別內部的私有成員,就如同朋友一樣。operator 要多載的運算子
: 例如string
的相加多載運算子就要寫成operator +
(const 物件參考 , const 物件參考 ,,,,,,)
:
這裡面是參數清單,最佳實踐是用 const
參考,而有幾個參數取決於運算子性質,例如一元運算子(e.g. 負號 -10 參數只有一個),這樣參數就只有一個。
而二元運算子 (e.g. 加號 1+2 參數有 1 和 2 有兩個),這樣參數就會有兩個,而順序就跟 1 跟2 一樣,例如 1+2
就會變成 operator+(1,2)
為甚麼要用參考? 因為有些時候物件很大,用參考可以減少複製的時間提升效率。
分數物件多載乘除實作
friend frac operator*(const frac& lhs, const frac& rhs) {
return frac(lhs.nu * rhs.nu, lhs.de * rhs.de);
}
// Friend function definition for division (inside the class)
friend frac operator/(const frac& lhs, const frac& rhs) {
if (rhs.nu == 0) {
throw std::invalid_argument("Cannot divide by zero-fraction.");
}
return frac(lhs.nu * rhs.de, lhs.de * rhs.nu);
}
其中 lhs
跟 rhs
是英文 左手邊跟右手邊的意思 ,例如 1/2+1/3
中,左手邊是 1/2
friend與
public
是可以存取 任何從frac類別實體化的物件 !! 不要誤會成他只能存取該物件自己,他是可以存取很多物件的,原因很簡單,因為她是在類別(模板、藍圖)定義,而每個根據該類別產生出來的物件都是相同類別的,當然也要被一視同仁。
分物數件輸入輸出實作
為了讓 frac
類別更完整,我們需要能夠方便地輸入和輸出分數。這可以透過多載 <<
(輸出運算子) 和 >>
(輸入運算子) 來實現。
C++
#**include** <iostream>
#**include** <sstream> // 用於 stringstream#**include** <numeric> // 用於 std::gcd (C++17) 或 __gcd (C++11/14)class frac {
.... 中略
public:
// ... (其他程式碼保持不變) ...
frac(int a, int b) {
this->nu = a;
this->de = b;
if (b == 0) {
throw std::invalid_argument("Denominator cannot be zero."); // 分母不可為0
}
reduce(); // 在建構子中加入化簡
}
// Friend function for output (<< operator)
friend std::ostream& operator<<(std::ostream& os, const frac& f) {
os << f.nu << "/" << f.de;
return os;
}
// Friend function for input (>> operator)
friend std::istream& operator>>(std::istream& is, frac& f) {
char slash; // 用來讀取 '/' 字元
is >> f.nu >> slash >> f.de;
if (slash != '/') {
is.setstate(std::ios::failbit); // 設定錯誤狀態
}
if (f.de == 0) {
throw std::invalid_argument("Denominator cannot be zero.");
}
f.reduce(); // 輸入後化簡
return is;
}
private:
int nu, de; // nu = numerator (分子), de = denominator (分母)
};
程式碼說明:
operator<<
(輸出運算子多載):friend std::ostream& operator<<(std::ostream& os, const frac& f)
:friend
: 允許這個函數存取frac
的私有成員。std::ostream& os
: 參考到一個輸出串流物件 (例如std::cout
)。const frac& f
: 參考到要輸出的frac
物件 (使用const
表示不會修改它)。os << f.nu << "/" << f.de;
: 將分數以 “分子/分母” 的形式輸出到串流。return os;
: 傳回輸出串流物件,以便進行串接 (例如std::cout << f1 << " " << f2;
)。
operator>>
(輸入運算子多載):friend std::istream& operator>>(std::istream& is, frac& f)
:friend
: 允許這個函數存取frac
的私有成員。std::istream& is
: 參考到一個輸入串流物件 (例如std::cin
)。frac& f
: 參考到要讀取資料的frac
物件 (這裡 沒有const
,因為我們會修改它)。char slash;
: 宣告一個字元變數來讀取預期的 ‘/’ 字元。is >> f.nu >> slash >> f.de;
: 從輸入串流讀取分子、斜線、分母。if (slash != '/') { is.setstate(std::ios::failbit); }
: 檢查是否成功讀取到 ‘/’,如果沒有,設定輸入串流的錯誤狀態。- 如果分母為0會丟出例外。
f.reduce()
:對分數進行化簡。return is;
: 傳回輸入串流物件,以便進行串接。
- 錯誤處理:
- 如果輸入格式不正確 (例如 “1/2/3” 或 “1a2”),
std::istream
的狀態會被設定為failbit
,表示輸入失敗。 - 如果分母為 0 會丟出例
- 如果輸入格式不正確 (例如 “1/2/3” 或 “1a2”),
使用範例:
C++
#**include** <iostream>
#**include** <stdexcept>// ... (frac 類別定義) ...
int main() {
frac f1(3, 6); // 建立分數 3/6
frac f2(1, -2); // 建立分數 1/-2
std::cout << "f1: " << f1 << std::endl; // 輸出: f1: 1/2
std::cout << "f2: " << f2 << std::endl; // 輸出: f2: -1/2
frac f3;
std::cout << "Enter a fraction (numerator/denominator): ";
std::cin >> f3; // 輸入分數
if (std::cin.fail()) {
std::cerr << "Invalid input format!" << std::endl;
} else {
std::cout << "You entered: " << f3 << std::endl; // 輸出輸入的分數
}
frac f4 = f1 * f2;
std::cout << "f1 * f2: " << f4 << std::endl;
try {
frac f5 = f1 / f2;
std::cout << "f1 / f2: " << f5 << std::endl;
} catch (const std::invalid_argument& e) {
std::cerr << "Error: " << e.what() << std::endl;
}
return 0;
}
總結
透過多載 <<
和 >>
運算子,我們可以像使用基本資料型態 (如 int
、double
) 一樣,自然地使用 std::cout
和 std::cin
來輸入和輸出 frac
物件。這使得程式碼更易讀、更易用。同時,加入了化簡和錯誤處理,使 frac
類別更加健壯。
與多型的關係
C++ 運算子多載是多型的一種,屬於編譯時多型(靜態多型/特定多型),是在 運算子上的多型 ,因為相同介面,但是不同操作,即用相同方法做不同事情。
它允許程式設計師重新定義運算子(如 +
、-
、==
)的行為,使其能用於自定義的類別。
編譯器在編譯階段會根據運算元的類型,決定使用內建的運算子定義,還是類別中多載的運算子函數(例如 string
+string
可以推論出其實要用字串加法,而不是內建的加法)。
這種機制讓程式碼更簡潔、直觀,因為可以用相同的運算子符號來操作不同類型的物件,實現不同的功能,展現了「多種形態」的概念,這正是多型的核心。