960 likes | 1.15k Views
21. 運算子重載. Follow me, and I will make you fishers of men. 《Matthew (4 : 19)》 當建立自己的類別時,可以使用運算子重載 (operator overloading) , 建立自己的運算符號系統。. 運算子重載. 21.1 運算子使用的基本概念 21.2 補充幾個類別使用上的要點 21.3 使用成員函數重載二元運算子 21.4 使用 friend 函數重載二元運算子 21.5 重載一元運算子 21.6 含有指標資料成員的類別 21.7 等效阻抗的計算.
E N D
21 運算子重載 Follow me, and I will make you fishers of men. 《Matthew (4:19)》 當建立自己的類別時,可以使用運算子重載 (operator overloading),建立自己的運算符號系統。
運算子重載 • 21.1 運算子使用的基本概念 • 21.2 補充幾個類別使用上的要點 • 21.3 使用成員函數重載二元運算子 • 21.4 使用friend函數重載二元運算子 • 21.5 重載一元運算子 • 21.6 含有指標資料成員的類別 • 21.7 等效阻抗的計算
21.1 運算子使用的基本概念 • 為自定的類別定義合用的運算符號常能簡化敘述,使程式的撰寫更簡潔,不易犯錯。例如 Vector A, B; float Answer = A*B; 而不用借助諸如下列的語法: float Answer = DotProduct(A, B);
二元的(binary)與一元的(unary) • 運算元 (operand): 運算子作用的對象。 • 二元運算子 (binary operator):每次都需要兩個運算元才能完成處理的運算子。例如:x*y • 二元運算子不可接連放置,例如 x*/y; // 錯誤的語法 • 一元運算子 (unary operator): 只和一個運算元作用的運算子。例如: -x
運算子的優先權(precedence of operators) • 當運算子不只一個時,常以小括號標明處理的先後。例如 (x+y)/(a+b) • 算術式 a*-b 則由於一元運算子「-」的優先權高於二元運算,它的處理程序相當於「a*(-b)」。
運算子的結合關係(associatively of operators) • 例如,二元運算子「*」和「%」的結合關係都是「和左邊的運算元結合」而且有相同的優先權。因此,算術式「a*b%c」的執行結果相當於 「(a*b)%c」。 • 並不是所有的符號都可以重載。 • 運算子() 有可能和左邊的運算元結合,譬如函數f(x) 和int(x),也有可能和右邊的運算元結合,例如(int)x,但是兩者具有不同的優先權。此外,「*」和「&」也有類似的情況。在這些場合,由於結合的關係不同,優先權也不同,必需視為不同的運算子。
運算子重載的限制 1.基本資料型態的運算子不可重載。 2.運算子的「優先權」、「結合關係」和「形式」都不可改變。 3.不可創造新的符號或結合兩個既有符號以定義成新的符號。 4.不在表內的符號不可使用。例如:「.」,「::」,「.*」,以及三元運算子「?:」都不能被重載。 5.運算子[ ] 最多只能接受一個參數,而運算子 ( ) 則對參數的數目沒有限制。
運算子重載的做法 • 運算子重載有兩種做法: • 1. 使用成員函數 (member function)。 • 2. 使用friend函數。 • 但兩種做法又依運算子是二元或一元的形式而有所不同。
自定類別MyClass • 類別MyClass的宣告如下: class MyClass { friend void F4(MyClass &); private: int Size; public: MyClass (int N): Size(N) {} ~MyClass() {} void F1(MyClass &); MyClass F2(); void F3(); } • 使用類別定義物件時,每個物件都擁有自己的資料成員,而同一類別的物件共用該類別的成員函數。
預設的成員函數(default member functions) • 編譯器 (compiler) 會自動加入四個成員函數: 1. 建構函數 (constructor) 2. 解構函數 (destructor) 3. 複製建構函數 (copy constructor) 4. 指派運算子 (assignment operator)
解構函數的自動呼叫 • 在區塊內定義的局部變數在離開其作用範圍 (scope) 時,就會自動呼叫。例如:
自定解構函數 • 解構函數是提供給程式設計者用來回收記憶資源的一種特殊語法。 • 如果類別的所有的資料成員都是屬於預設的資料型態,則不需要另外自行定義解構函數。 • 當類別中存在指標或字串、陣列這類衍生資料型態時,需要另外自行定義解構函數。
複製建構函數(copy constructor) • 使用已經存在的物件來建立新物件。例如: MyClass A(B); // 使用物件 B 來建立物件 A MyClass C = B;// 使用物件 B 來建立物件 C • 複製建構函數常以符號 X(X&) 來代表。在這個代表式裏X是假想的類別名稱,其參數使用同一個類別的參照 (reference)。
指派運算子(assignment operator) • 事實上就是我們熟悉的等號。例如: D = E; 就是標準的指派 (assignment) 運算。 • 如果資料成員內有指標時,這種單純的複製動作會發生問題。
隱藏的成員:指標this (1/2) • 使用public成員函數,例如: A.F3(); • 如果我們把建立A和F3()聯繫的動作明白地表示出來,則F3( ) 的原型可以寫成: void F3(MyClass* this);
隱藏的成員:指標this (2/2) • 這個this指標指向物件本身 (「this」是C++ 的關鍵字): 圖21.2.1this 指標 • 在本章的成員函數定義裏面,有一些處理程序必需用到這個this指標。
需要特別注意的Friend函數語法 1.它的宣告和public,private或protected無關,可以放在類別宣告內的任何地方。 2.「friend」這個關鍵字只需放在類別宣告的部份。 3.Friend函數的參數列中,至少要有一個該類別的物件,或該類別物件的參照 (reference)。 4.預設的四個成員函數都不可轉換為friend函數。
使用const參照的參數傳遞 • 傳參照 (pass-by-reference)時如果想要進一步避免原來的物件被修改,可以在參數列中使用const參照。例如: void F1(const MyClass&); • 使用const參照能兼顧效率和安全性。 • 複製建構函數 (copy constructor) X(X&) 也應該跟著修改成 X(const X&)。
定義新的類別Float • 以重新定義預設資料型態float對於「次方運算」的運算子。 • 在這個過程裏,我們可以仔細討論運算子重載的幾種基本語法。
和左側運算元結合的二元運算子 • 除了具有「指派」(assignment) 功能的運算符號,例如 =,*=,/=,<<=,>>=,&=,%=,-=,+=,^=,|= 之外,所有的二元運算子都和它左邊的運算元結合。 • 定義和左邊的運算元結合的二元運算子的成員函數時,只需要用到一個參數來代表符號右側的運算元。 • 省略的參數就是結合這個成員函數的物件本身。
重載二元運算子「+」和「^」 • 假設要定義的類別叫做Float,它有一個private資料成員 F: Float operator + (const Float& F2) const {return Float(F + F2.F); } Float operator ^ (const float& f) const {return Float(pow(F,f));} • 經由上式我們定義了兩個成員函數: operator +() operator ^() • 我們也可以使用「this」指標, 將 F + F2.F 寫成 this->F + F2.F 將 pow(F, f) 寫成 pow(this->F,f)
返回值最佳化 • operator +() 和operator ^() 的返回值 (return value)都是物件。例如,兩個成員函數的返回敘述 return Float(F+F2.F); return Float(pow(F,f)); 都呼叫了建構函數Float(float),以建立一個臨時的物件來容納計算結果。 • 這種在返回敘述內建立臨時的返回物件的做法,因為可以改善效率,稱為「返回值最佳化」 (return value optimization)。 • 並不是所有的成員函數都適用,只有處理程序單純的場合才用得著。
指派運算子「+=」的重載 • 指派運算子 (assignment operator)都和它右側的運算元結合。 • 以成員函數重載指派運算子時,需要右側的運算元做為其參數。 • 運算結果將改變運算子左側的運算元。例如: A += B; • 這個敘述將送回一個改變後的A,亦即 *this: Float& operator +=(const Float& F2) { F += F2.F; return *this; }
轉換運算子(conversion operator) • 將自定的物件轉換為C++ 內設的基本資料型態。例如: Float::operator float() 就是以內設的資料型態「float」做為轉換運算子的名稱。 • 轉換運算子只能以成員函數的形式定義。 • 轉換運算子在不同資料型態間進行「指派運算」時就會自動被呼叫。例如: float a; Float b(3.5); a = b; // 自動呼叫轉換運算子
成員函數operator float() 的定義 operator float() {return F;} • 注意,operator float() 的定義式不能再寫上返回資料型態: float operator float() {return F;} //錯誤!
標頭檔Float.h和主程式TestF.cpp • 包括類別Float的宣告和定義,使用成員函數重載運算子「+」,「*」,「^」和「+=」。 • 定義了轉換運算子float。 • 主程式TestF.cpp則是用來測試類別Float的正確性。 • 的確可以將運算符號「^」重新定義為次方運算,但代價是必需重新定義所有需要用到的運算子。
範例程式 檔案 Float.h (1/2) // Float.h #ifndef FLOAT_H #define FLOAT_H #include <iostream> using namespace std; class Float { private: float F; public: Float(float x) : F(x) {} Float(): F(0) {} // 定義 operator+() Float operator + (const Float& F2) const {return Float(F + F2.F); } // 定義 operator*() Float operator * (const Float& F2) const
範例程式 檔案 Float.h (2/2) {return Float(F * F2.F); } // 定義 operator^() Float operator ^ (const float& f) const {return Float(pow(F,f));} // 定義 轉換運算子 float operator float() {return F;} // 定義 operator += () Float& operator += (const Float& F2) { F += F2.F; return *this; } }; #endif
範例程式 檔案TestF.cpp // TestF.cpp #include "Float.h" int main() { Float f1(2.4), f2(3.5); cout << "f1 : " << f1 << endl; cout << "f2 : " << f2 << endl; cout << "f1+f2 : " << f1+f2 << endl; cout << "f1*f2 : " << f1*f2 << endl; cout << "f1^3.2: " << (f1^3.2) << endl; cout << "f1+=f2: " << (f1+=f2) << endl; return 0; }
21.4 使用friend函數重載二元運算子 • 運算子左右兩側的運算元都要列出當作參數。 • 除了運算子「+=」以外,「+」,「*」,「^」 三個運算子都不應該更動它左右兩側的運算元,因此這些運算子的參數列都是使用const參照。 • 這三個friend函數的原型如下所示: friend Float operator + (const Float& Left, const Float& Right); friend Float operator * (const Float& Left, const Float& Right); friend Float operator ^ (const Float& Left, const Float& Right);
三個friend函數的定義 Float operator+ (const Float& Left, const Float& Right) {return Float(Left.F + Right.F);} Float operator* (const Float& Left, const Float& Right) {return Float(Left.F * Right.F);} Float operator^ (const Float& Left, const Float& Right) {return Float(pow(Left.F, Right.F));} • 這些定義都符合上節「返回值最佳化」的原則。
operator +=() 的定義 • 代表運算子左側物件的參數必需使用參照 (reference),不能是const參照。 • 其返回值也應該是參照,才不會再另行創造一個物件。 • 這個friend函數的原型如下所示: friend Float & operator += (Float& Left, const Float& Right); • 函數定義: Float& operator += (Float& Left, const Float& Right) { Left.F += Right.F; return Left; }
標頭檔Float.h和主程式TestF.cpp • 標頭檔FloatNM.h包括類別Float的宣告和定義,使用friend函數重載運算子「+」,「*」,「^」和「+=」。 • 主程式TestF2.cpp用來測試FloatNM.h的正確性。
範例程式 檔案 FloatNM.h (1/3) // FloatNM.h #ifndef FLOATNM_H #define FLOATNM_H #include <iomanip> using namespace std; // ---- 類別Float的宣告 ----------------------------- class Float { friend Float operator + (const Float& Left, const Float& Right); friend Float operator * (const Float& Left, const Float& Right); friend Float operator ^ (const Float& Left, const Float& Right); friend Float& operator += (Float& Left, const Float& Right);
範例程式 檔案 FloatNM.h (2/3) private: float F; public: Float(float x) : F(x) {} Float(): F(0) {} // -- 定義 資料型態轉換運算子 float ------------- operator float() {return F;} }; // -- 定義 operator+() ------------------------ Float operator + (const Float& Left, const Float& Right) {return Float(Left.F + Right.F);}
範例程式 檔案 FloatNM.h (3/3) // -- 定義 operator*() ------------------------- Float operator * (const Float& Left, const Float& Right) {return Float(Left.F * Right.F);} // -- 定義 operator^() ------------------------- Float operator ^ (const Float& Left, const Float& Right) { return Float(pow(Left.F, Right.F));} // -- 定義 operator+=() ------------------------ Float&operator += (Float& Left, const Float& Right) { Left.F += Right.F; return Left;} #endif
範例程式 檔案 TestF2.cpp // TestF2.cpp #include "FloatNM.h" int main() { Float f1(2.4), f2(3.5); cout << "f1 : " << f1 << endl; cout << "f2 : " << f2 << endl; cout << "f1+f2 : " << f1+f2 << endl; cout << "f1*f2 : " << f1*f2 << endl; cout << "f1^3.2: " << (f1^3.2) << endl; cout << "f1+=f2: " << (f1+=f2) << endl; return 0; }
21.5 重載一元運算子 • 前置,prefix: 運算子在運算元之前,所需要的參數數目比二元運算子少一個。 • 後置,postfix:運算子在運算元之後 (與左側的運算元結合),參數的數目和二元運算子相同。列表如下:
使用成員函數重載前置一元運算子 • 重載一元運算子負號和前置累加,亦即operator -() 和operator ++() 的語法: Float operator -() const // 定義 Prefix - { return Float(-F); } Float& operator ++() // 定義 Prefix ++ { ++F; return *this; } • operator -() 的運算並不改變原有物件,因此宣告為const成員函數。 • 前置運算子operator ++()則會改變原有物件,因此雖然參數列是空的,其傳回值是 *this,且返回的資料型態為參照「Float &」。
使用成員函數重載後置一元運算子 • 後置一元運算子operator ++() 使用成員函數定義的語法: Float operator ++(int) // 定義 Postfix ++ { Float Before(F); F++; return Before; } 在參數列中有一個int的額外資料型態,稱為啞常數 (dummy constant value)。 • 這個運算要先將原有值返回,之後才將原來物件的值改變。使用friend函數重載前置一元運算子
使用friend函數重載前置一元運算子 • 參與的運算元必需明白的寫出來,並在函數本體內使用「物件名稱 + 成員運算子(.) + 資料成員」的語法。 • 前置運算子operator !()定義為將內值平方,而前置運算子operator --() 則為累減。原型如下: friend Float operator ! (const Float& F1); friend Float& operator --(Float& F1); • 定義如下: Float operator ! (const Float& F1) { return Float(F1.F*F1.F); } Float& operator -- (Float& F1) { F1.F--; return F1; }
使用friend函數重載前置一元運算子 • 後置一元運算子operator --() 的宣告: friend Float operator -- (Float& F1, int); • 後置運算子operator --() 的定義: Float operator --(Float& F1, int) { Float Before(F1.F); F1.F--; return Before; }
檔案UnaryF.h 和TestUnary.cpp • 所有關於一元運算子重載的語法。在這個檔案裹面,我們對於各種一元運算子都做了全面性的示範,如表21.5.2所示。 表21.5.2 檔案UnaryF.h內所定義的一元運算子 • TestUnary.cpp是用來測試檔案UnaryF.h。
範例程式 檔案 UnaryF.h // UnaryF.h #ifndef UNARYF_H #define UNARYF_H #include <iostream> using namespace std; class Float { // 使用 friend 函數宣告 prefix operator! () friend Float operator ! (const Float& F1); // 使用 friend 函數宣告 Prefix operator--() friend Float& operator --(Float& F1); // 使用 friend 函數宣告 Postfix operator--() friend Float operator --(Float& F1, int); private: float F; public: // --- 定義 Float 的建構函數 ------------------------ Float(float x) : F(x) {} Float(): F(0) {}
範例程式 檔案 UnaryF.h // -- 定義 轉換運算子 float ------------------------- operator float() {return F;} // -- 使用成員函數定義 prefix operator - () Float operator -() const {return Float(-F);} // -- 使用成員函數定義 Prefix operator ++ () Float& operator++() { ++F; return *this; } // -- 定義 postfix operator ++ () Float operator++(int) { Float Before(F); F++; return Before; } };