Class(二) Overloaded Operators User-Defined Conversions 前言 超載運算子 物件導向程式語言講義 2018/12/2 Class(二) Overloaded Operators 前言 超載運算子 超載運算子的定義方式 超載運算子的範例 User-Defined Conversions 靜宜大學資訊管理學系 蔡奇偉 副教授
前言 C++ 允許你為自定的資料型態(enum 或 class)定義運算子,使其能夠像基本資料型態一般地以算式的形式來處理物件。你只能 overload C++ 現有的運算子,而不能自創運算子。比方說,你不能定義 ** 運算子,因為它並不是 C++ 的運算子。我們在下一頁中,列出 C++ 所允許和不允許 overloaded 的運算子。 此外, overloaded 運算子的運算優先順序完全遵照 C++ 既有的規定;你無法做任何的變更。同樣地,你也不能把原先是單元運算子(如 ~)定義成二元運算子,反之亦然。
超載運算子 C++ 允許超載運算子如下: + - * / % ^ & | ~ + - * / % ^ & | ~ ! , = == != < > <= >= ++ -- << >> && || += -= *= /= %= ^= &= |= <<= >>= [] () -> ->* new new[] delete delete[] C++ 不允許超載的運算子有四: :: .* . ?:
超載運算子的定義方式 二元運算子 假定 @ 代表一個二元運算子、X 和 Y 是兩個類別。你可以用以下兩種方式來宣告 X 和 Y 之間的運算 @: 成員運算子: class X { public: return_type operator@ (Y); }; 非成員運算子: return_type operator@ (X, Y);
如果兩種運算子的宣告你都提供,則對算式 aa @ bb 而言,C++ 編譯器會根據 aa 和 bb 的類型,解讀成下面兩種函式呼叫中的一種: aa.operator@(bb) // 呼叫成員運算子 operator@(aa, bb) // 呼叫非成員運算子 譬如: class X { public: X(int); void operator+ (int); }; void operator+ (X, X); void operator+ (X, double); void f (X a) { a + 1; // a.operator+(1) 1 + a; // ::operator+(X(1), a) a + 1.0; // ::operator+(a, 1.0) }
prefix 單元運算子 假定 @ 代表一個 prefix 單元運算子(如 ~ 和 ++k 的 ++)、X 是一個類別。你可以用以下兩種方式來宣告 X 的運算 @: 成員運算子: class X { public: X operator@ (); // no argument }; 非成員運算子: X operator@ (X); // one argument C++ 編譯器會根據 aa 的類型把 @aa 解讀成 aa.operator@() 或 operator@(aa) 。
postfix 單元運算子 假定 @ 代表一個 postfix 單元運算子(如 k++ 的 ++)、X 是一個類別。你可以用以下兩種方式來宣告 X 的運算 @: 成員運算子: class X { public: X operator@ (int); // one argument }; 非成員運算子: X operator@ (X, int); // two argument C++ 編譯器會根據 aa 的類型把 aa@ 解讀成 aa.operator@(int) 或 operator@(aa, int) 。
一般而言,你應該把運算值為 lvalue 的運算子定義成類別的成員。這些運算子包括: = [] () -> += -= *= /= %= ^= &= |= <<= >>= 等等。其中前四者一定要定義成類別的成員。 其他運算值為 rvalue 的運算子則最好不要定義成類別的成員,如:+, -, *, / 等。
範例 class X { X* operator& (); // prefix unary & (address of) X operator& (X); // binary & (and) X operator++ (int); // postfix increment X operator& (X, X); // error: ternary X operator/ (); // error: unary }; X operator- (X); // prefix unary minus X operator- (X, X); // binary minus X operator-- (X&, int); // postfix decrement X operator- (); // error: no operand X operator- (X, X, X); // error: ternary X operator% (X); // error: unary %
超載運算子的範例 接下來我們用 CPoint2D 和 CString 兩個類別來示範如何設計超載運算子。前者我們已在上一章定義過它的結構。後者是模擬 C++ 標準類別庫中的 string 類別。它的基本結構如下: class CString { public: CString (const char * = 0); // default constructor CString (const CString &); // copy constructor ~CString (); // destructor char *c_str() { return (_size)? _string : “”; } private: int _size; char *_string };
// File: CString.cpp #include <string.h> CString::CString (const char *s) { if (s) { _size = strlen(s); _string = new char[_size+1]; strcpy(s, _string); } else { _size = 0; _string = 0; } CString::CString (const CString &s) delete [] _string; _size = s._size; strcpy(s._string, _string); CString::~CString () { delete [] _string; _size = 0; _string = 0; }
Assignment Operator = 指定運算子(=)是用來把等號右邊的物件值複製給等號左邊的物件。若沒有定義此運算子時,C++ 會自動提供一個預設的指定運算子,把等號右邊物件的資料成員一一地複製給等號左邊物件中對應的資料成員。比方說,CPoint2D 類別若沒有定義指定運算子,則 CPoint2D a(3, 4), b(4, 5); b = a; // b 變成 (3, 4),因為 b._x = a._x; b._y = a._y; 這樣的指定方式正好適用,因此我們就不需要再替CPoint2D 類別定義指定運算子。
但是對 CString 這種擁有動態配置記憶體的類別,預設的指定運算子顯然就不適用了(理由請參閱上一章有關 copy constructor 的討論)。因此我們必須為 CString 類別定義適用的指定運算子。 X 類別的指定運算子宣告方式如下: class X { public: X& operator= (const X&); // … } 註:如前所述,operator= 必須定義成類別的成員函式。
CString& CString::operator= (const CString &s) { if (this != &s) { // not assign to self delete [] _string; _size = s._size; if (_size) { _string = new char[_size+1]; strcpy(s._string, _string); } else _string = 0; return *this;
我們來探討以下的指定運算: CString s1(“Hello”); // 呼叫 CString(“Hello”) 來設定 s1 的初值 CString s2, s3; // 呼叫 CString(0) 來設定兩者的初值 s2 = s1; // 呼叫 s2.operator=(s1),故 s2 = “Hello” s3 = “World”; // s3.operator=(CString(“World”)) 最後一行的指定運算先呼叫 CString(“World”) 把字串 “World” 轉換成 CString 物件,然後再呼叫 s3.operator= 把此物件指定給 s3。顯然這個指定過程的效率並不好。由於指定字串給CString 物件是一項使用頻繁的算式,似乎值得我們專門為這種形式的指定運算子寫一個函式。
有了以上的 operator= 定義之後,前一頁的 s3 指定就變成: CString& CString::operator= (const char *s) { delete [] _string; _size = strlen(s); if (_size) { _string = new char[_size+1]; strcpy(s, _string); } else _string = 0; return *this 有了以上的 operator= 定義之後,前一頁的 s3 指定就變成: s3 = “World”; // s3.operator=( “World”) 而省掉呼叫 CString(“World”) 的過程。
Operator += CPoint2D 的 += 運算子 class CPoint2D { public: CPoint2D& CPoint2D::operator+= (const CPoint2D &); … }; CPoint2D& CPoint2D::operator+= (const CPoint2D &p) { _x += p._x; _y += p._y; return *this; }
測試程式 #include <iostream> #include “CPoint2D.h” int main () { CPoint2D a(3,4), b(5,6); a += b; cout << a; } 輸出結果: (8, 10)
CString 的 += 運算子 class CString { public: CString& CString::operator+= (const CString &s); { return concat(s._string, s._size); } CString& CString::operator+= (const char *s) { return concat(s, strlen(s)); } … private: CString& concat (const char *, int); };
CString& CString::concat (const char *s, int len) { if (len > 0) { char *tmp = _string; // backup the old string _size += len; _string = new char[_size+1]; strcpy(tmp, _string); strcat(_string, s); delete [] tmp; // delete the old string } return *this;
Operator + CPoint2D 的 + 運算子 class CPoint2D { public: friend CPoint2D operator+ (const CPoint2D &, const CPoint2D &); … }; CPoint2D operator+ (const CPoint2D &p1, const CPoint2D &p2) { CPoint2D p(p1); p._x += p2._x; p._y += p2._y; return p; }
CString 的 + 運算子 class CString { public: friend CString operator+ (const CString &cs1, const CString &cs2); friend CString operator+ (const CString &cs, const char *s); friend CString operator+ (const char *s, const CString &cs); friend CString cs_add (const char *, int, const char *, int); … }; 這些 + 運算子允許以下的算式: CString s1, s2; s1 + s2 s1 + “July” “July” + s1
// File: CString.cpp CString cs_add (const char *s1, int len1, const char *s2, int len2) { CString sobj; sobj._size = len1 + len2; if (sobj._ size) { sobj._string = new char[sobj._ size+1]; strcpy(“”, sobj._string); if (s1) strcat(sobj._string, s1); if (s2) strcat(sobj._string, s2); } return sobj;
// File: CString.h inline CString operator+ (const CString &cs1, const CString &cs2) { return cs_add(cs1._string, cs1._size, cs2._string, cs2._size) } inline CString operator+ (const CString &cs, const char *s) return cs_add(cs._string, cs._size, s, strlen(s)) inline CString operator+ (const char *s, const CString &cs) return cs_add (s, strlen(s) , cs._string, cs._size)
Operator ==, != CPoint2D 的 == 和 != 運算子 class CPoint2D { public: friend bool operator== (const CPoint2D &, const CPoint2D &); friend bool operator!= (const CPoint2D &, const CPoint2D &); … }; bool operator== (const CPoint2D &p1, const CPoint2D &p2) { return (p1._x == p2._x && p1._y == p2._y); } bool operator!= (const CPoint2D &p1, const CPoint2D &p2) return (p1._x != p2._x || p1._y != p2._y);
CString 的 == 和 != 運算子 class CString { public: friend bool operator== (const CString &cs1, const CString &cs2); friend bool operator== (const CString &cs, const char *s); friend bool operator== (const char *s, const CString &cs); friend bool operator!= (const CString &cs1, const CString &cs2); friend bool operator!= (const CString &cs, const char *s); friend bool operator!= (const char *s, const CString &cs); friend bool cs_equal (const char *, int, const char *, int); … };
// File: CString.cpp bool cs_equal (const char *s1, int len1, const char *s2, int len2) { if (len1 != len2) return false; else if (s1 == 0 || s2 == 0) return true; else return (strcmp(s1, s2) == 0); }
// File: CString.h inline bool operator== (const CString &cs1, const CString &cs2) { return cs_equal(cs1._string, cs1._size, cs2._string, cs2._size) } inline bool operator== (const CString &cs, const char *s) return cs_ equal(cs._string, cs._size, s, strlen(s)) inline bool operator== (const char *s, const CString &cs) return cs_ equal(s, strlen(s), cs._string, cs._size)
// File: CString.h inline bool operator!= (const CString &cs1, const CString &cs2) { return !cs_equal(cs1._string, cs1._size, cs2._string, cs2._size) } inline bool operator!= (const CString &cs, const char *s) return !cs_ equal(cs._string, cs._size, s, strlen(s)) inline bool operator!= (const char *s, const CString &cs)
Operator [] CPoint2D 的 [] 運算子 左邊的設計讓 [] 運算子可以用來存取 CPOint2D 物件的 x 座標值與 y 座標值。譬如: CPoint2D p(3,4); p[0] = p[0] + p[1]; p[1] = p[0] - p[1]; cout << p; 輸出的結果為:(7, 3) class CPoint2D { public: int& operator[] (int index) { assert(index == 0 || index == 1); return (index == 0) ? _x : _y; } … };
CString 的 [] 運算子 左邊的設計讓 [] 運算子可以用來存取 CString 物件的個別字元值。譬如: class CString { public: char& operator[] (int index) { assert(index >= 0 || index < _size); return _string[index]; } … }; 左邊的設計讓 [] 運算子可以用來存取 CString 物件的個別字元值。譬如: CString s(“howdy!”); s[2] = ‘x’; cout << s << s[0]; 輸出的結果為:hoxdy!h (假定 CString 支援 << 輸出運算子)
Operator () operator () 通常用來定義所謂的函式物件(function object)。譬如: class AbsInt { public: int operator () (int val) { return (val >= 0)? val : -val; } }; 則底下程式的輸出為:10 and 10 AbsInt a; cout << a(10) << “ and “ << a(-10);
User-Defined Conversions 如果有必要的話,我們可以為類別設計資料轉換的方式。假定 X 代表一個類別。X 單一參數的建構函式可視為一種資料轉換的函式,可用來把其它型態的資料轉換成 X 物件。譬如: class Y; class X { public: X (int); // 把 int 轉換成 X 物件 X (float); // 把 float 轉換成 X 物件 X (Y); // 把 Y 物件轉換成 X 物件 … };
反過來說,我們也可以用轉換運算子(conversion operator)把 X 類別的物件轉換成其它型態的物件。轉換運算子是 X 類別的一種成員函式,其格式如下: operator T (); 其中的 T 代表一個資料型態的名稱。這樣的轉換運算子是用來把 X 類別的物件轉換成 T 型態的物件。譬如: class Y; class X { public: operator int (); // 把 X 物件轉換成 int operator float (); // 把 X 物件轉換成 float operator Y (); // 把 X 物件轉換成 Y 物件 … };
轉換運算子 operator T () 必須滿足下列的條件: 轉換運算子必須是成員函式。 轉換運算子不可指定傳回值的型態。 轉換運算子不可有參數。 T 可以是 C++ 內建的基本資料型態、其它類別的名稱、或 typedef 的名稱。 T 不可以是陣列型態或函式型態。
operator int (X); // error: 不是成員函式 typedef int[100] iarray; typedef (*int)() funcptr; class X { public: int operator int (); // error: 不可有傳回值型態 operator int (int); // error: 不可有參數 operator iarray (); // error: 不可是陣列型態 operator funcptr (); // error: 不可是函式型態 // … };
範例