--- title: 'Class 運用 - operator、friend、explicit' disqus: kyleAlien --- Class 運用 - operator、friend、explicit === ## OverView of Content 如有引用參考請詳註出處,感謝 :smile: [TOC] ## Refernce & 副本 * 返回使用 reference & 副本的差別在,副本是函式內創建靜態變數 (無連結),**在 ==Func 結束前會產生其副本== 讓呼叫的函式取得**,最後函式結束後釋放靜態區域變數 :::warning **使用哪個要依照需求而定,不能一味地看效率** ::: ```cpp= /** * 宣告 */ class Score { private: int score; public: Score(int score = 0); // 預設引數 const Score& Sum_Ref(Score& s) const; // Refernce 函數 const Score Sum_Cpy(const Score& s) const; // 副本 int getScore() { return score; }; }; // -------------------------------------------------------------------- /** * 實做 */ #include "TestClass.h" Score::Score(int score) { this->score = score; } const Score& Score::Sum_Ref(Score& s) const { // "1. " s.score += this->score; return s; } const Score Score::Sum_Cpy(const Score& s) const { // "2. " Score c; // default construct c.score = this->score + s.score; return c; // 返回前會呼叫 Score 複製函數 } // -------------------------------------------------------------------- /** * 客戶端使用 */ #include "TestClass.h" int main() { Score s1(30), s2(30), s3(30), s4(30); Score result_1 = s1.Sum_Ref(s2); //"3. " std::cout << "s1 score: " << s1.getScore() << ", s2 score: " << s2.getScore() << ", After Ref sum score: " << result_1.getScore() << std::endl; Score result_2 = s3.Sum_Cpy(s4); std::cout << "s3 score: " << s3.getScore() << ", s4 score: " << s4.getScore() << ", After Ref sum score: " << result_2.getScore() << std::endl; return 0; } ``` 1. `Sum_Ref` 宣告為**返回一個 Reference 物件**,並且引數為可改變的物件,==效率較高== 2. `Sum_Cpy` 宣告為返回一個物件,該物件是 **==新的物件==,其++生命週期在 Function 開始到結束++**,在**釋放記憶體前 return 物件,複製該物件內容到呼叫函數取得**,==效率較低== 3. 改變 s2 的內容,因為是傳 reference 進入不會新建物件 **--實作--** > ![](https://i.imgur.com/fWmTvzQ.png) ## operator 運算子多載 * operator 運算子多載可以讓使用起來更好看 (儘管 function name 就是 opeator 很奇怪) ;運算子函數的規格,**operator ==++op++==(argument...)**,**op 是運算符號,但++它還是一個函數++** :::warning **不能多載 C++ 沒有的運算子** ::: C++ 運算子可以有多種意義 (如同函數多載),**C++ 會依據==引數的數量== & ==型態==來決定使用哪個運算子** ```cpp= /** * 宣告 */ class Score { private: int score; public: Score(int score = 0); const Score& Sum_Ref(Score& s) const; const Score Sum_Cpy(const Score& s) const; const Score operator+(const Score& s) const; // 新增 int getScore() { return score; }; }; /** * 實作 新增 operator+,有無空白都可以 operator + 也可 */ const Score Score::operator+(const Score& s) const { //"1. " Score c; // default construct c.score = this->score + s.score; return c; } /** * 客戶端使用 */ #include "TestClass.h" int main() { Score s1(30), s2(50); Score result_1 = s1.Sum_Cpy(s2); std::cout << "After function sum score: " << result_1.getScore() << std::endl; Score result_2 = s1 + s2; //"1. " std::cout << "After + sum score: " << result_2.getScore() << std::endl; Score result_3 = s1.operator +(s2); //"2. " std::cout << "After operator+ sum score: " << result_3.getScore() << std::endl; return 0; } ``` 1. 其內部實現如同 `SumCpy`,創建一個新物件 2. 可以用 func 全名,**`operator+` 就是 func 名**,功能如同**使用 + 號**,\+ 號是 operator 的簡寫 **--實作--** > ![](https://i.imgur.com/ox2s4X9.png) ### operator 函數回傳值 - 重複使用 * 這要考慮到,**符號的==結合性==**,而 **加號的結合性為 ==左到右==**;如果上面使用 `s1 + s2 + s3` 會轉換成 1. **s1.opertor**(s2 + s3); > 由最左邊的 s1 開始轉換 operator() 2. s1.opertor(**s2.opertor**(s3)); 以此類推 > 再轉換到 s2 operator() ```cpp= #include "TestClass.h" int main() { Score s1(30), s2(50), s3(70); // 直接呼叫副本 Function Score result_1 = s1.Sum_Cpy(s2).Sum_Cpy(s3); std::cout << "After function sum score: " << result_1.getScore() << std::endl; // 使用運算符重載 Score result_2 = s1 + s2 + s3; std::cout << "After + sum score: " << result_2.getScore() << std::endl; // 呼叫運算符重載 function Score result_3 = s1.operator +(s2).operator +(s3); std::cout << "After operator+ sum score: " << result_3.getScore() << std::endl; return 0; } ``` **--實作--** > 新增 s3,並連續加法,三種方法都可以得到相同結果 > > ![](https://i.imgur.com/EF1KNRz.png) ### 多載限制 - operator 限制 1. **不能違反原始運算子的 ++語法規則++** ```cpp= int x; Score s; % x; // 原始操作 不合法 % s; // 使用 operator 複寫,但操作 不合法 ``` 2. **不能產生新的符號** > Ex: 想產生一個沒有的 `__` 符號 operator__(),這會導致編譯失敗 3. **引數至少要有一個使用者自定義型態**,這樣才不會與原始符號衝突 (也就是要有一個參數) > Ex: operator+(const Score& s) * 還有以下的符號 **不能重載 !** | 運算子 | 描述 | | ---------------- | ------------------------------ | | sizeof | 計算該類別佔多少 byte 記憶體 | | . | 成員運算子,struct、class 使用 | | .* | 成員指標 運算子 | | :: | ==範疇== 運算子 | | ?: | 三元條件 運算子 | | typeid | RTTI 運算子 | | const_cast | 轉換 const 型態 | | dynamic_cast | 轉換 dynamic 型態 | | reinterpret_cast | 轉換 reinterpret 型態 | | static_cast | 轉換 static 型態 | * 以下運算子 **++不能透過非成員函數++ 加載 (也就是 friend 函數不可加載)**,會破壞順序 | 運算子 | 描述 | | -------- | -------- | | = | 指定 運算子 | | () | 函數呼叫 運算子 | | [] | ==標註== 運算子 | | -> | 指標間接抓取 運算子 | ## friend 夥伴函數 之前有提到,**存取 private 成員資料的唯一方式是透過函數**,但還有一個方法可以提取到私有變量,就是 **使用 friend 宣告的函數,==friend 函數可取得 private 變量==** :::info 記憶:因為是好朋友,所以可以使用自身的私有物品 ::: * friend 有三種變形 1. 夥伴 **函數** 2. 夥伴 **類別** 3. 夥伴 **成員函數** * 需要 friend 的原因在於,類別多載二元運算時 (兩個引數稱二元) 產生的例外狀況 ```cpp= // 以上面的 Score 為例, 新增一個普通變數 // 原型 const Score operator+(const int i) const; // 實作 const Score Score::operator +(int i) const { Score c; // default construct c.score = this->score + i; return c; } // 客戶端 int main() { Score s1(30); Score result_1 = s1 + 10; // 一般的正常用法,但如果反過來加呢 ? 10 + s1 std::cout << "result: " << result_1.getScore() << std::endl; return 0; } ``` 該呼叫是透過 s1 的方法 `operator+()`,但是如果反向過來變成 `10 + s1`,以語意來說是正確的,但是 **10 並不是類型,它沒有 operator+()** !! ### friend 宣告 * **==friend 是非成員函數==,定義時++不使用 :: (範疇運算)++** > 非成員函數 : 不是用物件呼叫 (Call Function),它所擁有的引數都是顯示 (包括沒有 this) * 在**類別內宣告 friend 函數,該函數就可以使用 private 的成員** ```cpp= /** * 新增 10 + obj 的宣告 */ class Score { private: int score; public: Score(int score = 0); const Score& Sum_Ref(Score& s) const; const Score Sum_Cpy(const Score& s) const; const Score operator+(const Score& s) const; const Score operator+(const int i) const; // "1. " 新增 friend 非成員函數 friend const Score operator+(const int i, const Score & s); // const; int getScore() { return score; }; }; // -------------------------------------------------------------------- /** * 實現 */ // 這裡不能出現 friend 關鍵字 const Score operator +(const int i, const Score & s) { //return s.operator +(i); "Same as s + i" return s + i; //"2. " 反過來使用 Score 的 operator= 函數 } // -------------------------------------------------------------------- /** * 客戶端使用 */ #include "TestClass.h" int main() { Score s1(30); Score result_1 = s1 + 10; std::cout << "result: " << result_1.getScore() << std::endl; Score result_2 = 30 + s1; //"3. " std::cout << "result: " << result_2.getScore() << std::endl; Score result_3 = operator+(30, s1); std::cout << "result: " << result_3.getScore() << std::endl; return 0; } ``` **--實作--** > ![](https://i.imgur.com/FmVRwjD.png) 1. **宣告時需要 friend,定義時不用 firend**,**並且 ==非成員函數不可使用 [const 修飾 func](https://hackmd.io/32uk2K2ZSUKoCKYlidy-Dg?both#const-成員函數)==,因為沒有可以修飾的對象** ```cpp= // 不能用 const,因為 Score 非自身的物件 friend const Score operator+(const int i, const Score & s) const; ``` :::warning * **friend 這個關鍵字只能用 (出現) 在 class 中**,用在定義就會錯誤 (如下圖) > ![](https://i.imgur.com/BoxRNsg.png) ::: 2. 既然 `friend` 可以使用該對象的資源,代表也可以使用對象的 function,那就可以直接反向後使用 3. **`30 + s1` 等價於` operator*(30, s1)`** ### friend & OOP 關係 * ++乍看之下 friend 似乎破壞了 OOP 的資料隱藏規則++,但其實 **它應該看做類別的延伸介面**,因為 **如果沒有了 `friend` 宣告,使用者也不能使用,==`friend` 宣告即可看成介面的延伸==** * Class 宣告哪些函數可以存取 private 資料,而 `friend 函數非成員函數`、`類別函數`,**兩者是表示類別介面的兩種不同機制** ```cpp= // 類別函數、成員函數 const Score operator+(const int i) const; // 非成員函數 friend const Score operator+(const int i, const Score & s); ``` ### 多載輸出 `<<` 運算子 * `<<` 除了在 C 中的基礎位移 (向左位移);C++ 中的 `<<` 運算子還可以使用在多種基本型態,這是因為 **`<<` 運算子多載了許多基礎類型** ```cpp= // 在 Score 中多載 << 運算子,讓它自動輸出需要的資料 /* * 宣告 */ class Score { private: int score; public: Score(int score = 0); const Score& Sum_Ref(Score& s) const; const Score Sum_Cpy(const Score& s) const; const Score operator+(const Score& s) const; const Score operator+(const int i) const; // why use friend ? friend const Score operator+(const int i, const Score & s); // Write friend of new << friend std::ostream& operator<<(std::ostream &os, const Score & s); int getScore() { return score; }; }; /* * 定義 */ std::ostream& operator<<(std::ostream &os, const Score &s) { os << "score: " << s.score << " "; return os; } /* * 客戶端使用 */ #include "TestClass.h" int main() { Score s1(30); Score result_1 = s1 + 10; Score result_2 = 30 + s1; Score result_3 = operator+(80, s1); std::cout << result_1 << result_2 << result_3 << std::endl; return 0; } ``` 使用 firend 函數的原因:**不能使用類別函式,但是又必須使用 class 的成員 Score,所以使用 friend**;不用在 ostream 原因很簡單,因為不好維護,不可能因為新增一個類別就去修改 ostream 對象 > 類別函式 : 沒有人在使用 result_1 <<,所以不能使用 ### 成員函數 vs 非成員函數 | Type | 引數 | Example | | -------- | -------- | -------- | | 成員函數 | ==有隱式引數== | operator+(int i) + 隱式 this | | 非成員函數 | ==都是顯示引數== | operator+(int i, Object & o) 引數都是顯式 | * 並且不可同時定義兩個相同格式的函數,**會導致==模糊性錯誤==**,編譯期間就會錯誤 ```cpp= S1 = S2 + S3; S1 = S2.operator+(S3); // 成員函數 S1 = operator+(S2, S3); // 非-成員函數 ``` ## 類別自動轉換 & 類型轉換 * 類別的下一個主題是型態轉換,**C++ 內建型態轉換,++當一個敘述是標準型態,指定給另一個標準型態時可以自動轉換++** (**==前提兩個類型要相容==**) ```cpp= int main() { long count = 8; double time = 11; int side = 3.33; std::cout << "long : " << count << std::endl; std::cout << "double : " << time << std::endl; std::cout << "int : " << side << std::endl; int * p1 = 10; // "1. " int * p2 = (int *) 10; // "2. " std::cout << "ptr1 : " << p1 << std::endl; std::cout << "ptr2 : " << p2 << std::endl; // ptr2 : 0x0a return 0; } ``` 1. 類型不相同,無法相互轉換,`int *` **指標雖然在電腦中是使用整數儲存,但是 ==指標與整數的概念完全不一樣==** 2. 仍然有方法可以透過轉換,讓 10 (整數型態 int) 強制轉換成指標 `(int*)` **--實作--** > ![](https://i.imgur.com/UqSAmYQ.png) ### 類別型態建構 & 單一引數 & 轉換函數 * 在 C++ 中,**任何有 ++單一引數++ 的 construct,就像是==將該引數值,轉換成++類別型態++==**,該 **++單一引數的建構函數++ 就可作為 ==轉換函數==** :::info 透過**類別型態找尋建構函數,建構出物件的過程稱為 ==自動轉換==** ::: ```cpp= // 以下範例使用 石 & 磅 的轉換 /** * class 宣告 */ #include "iostream" class stonewt { private: static const int Lbs_per_stn = 14; // 石 : 磅 = 1 : 14 int t_value = 0;// test 值是否被複製 int stone; double p_lbs; // point lbs double t_lbs; // total lbs public: stonewt(); stonewt(double lbs); // 磅 //"4. " stonewt(int stone, double lbs); // 石 + 磅 ~stonewt(); void Set_Value(int v); void Show_Value(); void Show_Lbs(); void Show_Stone(); }; // ----------------------------------------------------------------- /** * 定義 */ #include "stonewt.h" stonewt::stonewt() { std::cout << "construct...empty" << std::endl; stone = t_lbs = p_lbs = 0; } stonewt::stonewt(double lbs) { std::cout << "construct...lbs" << std::endl; this->stone = int(lbs) / Lbs_per_stn; // 石 整數 this->p_lbs = (int(lbs) & Lbs_per_stn) + (lbs - int(lbs)); // 磅 小數 this->t_lbs = lbs; // 全部磅數 } stonewt::stonewt(int stone, double lbs) { std::cout << "construct...stone & lbs" << std::endl; this->stone = stone; this->p_lbs = lbs; this->t_lbs = stone * Lbs_per_stn + lbs; } stonewt::~stonewt() { std::cout << "...destruct, address: " << this << std::endl; } void stonewt::Show_Lbs() { std::cout << "total lbs: " << t_lbs << std::endl; } void stonewt::Show_Stone() { std::cout << "total stone: " << stone << "." << p_lbs << std::endl; } void stonewt::Set_Value(int v) { t_value = v; } void stonewt::Show_Value() { std::cout << "value: " << t_value << std::endl; } // ----------------------------------------------------------------- /** * 客戶端使用 */ #include "./typeChange/stonewt.h" int main() { stonewt s; // "1. " s.Set_Value(100); s.Show_Value(); std::cout << "原來物件地址: " << &s << std::endl; std::cout << "\n使用 \"=\" 指定" << std::endl; s = 19.6; // "2. " 19.6 會創建 暫時物件! s.Show_Value(); // "3." return 0; } ``` 1. 使用 default 建構函數創建 (無引數 construct) 2. 當在使用 **==等號==賦予值,就會==再次創建一次暫時物件==,並使用 double 引數的創建函數,並使用 19.6 作為初始值,該物件只是暫時的** > 暫時物件在賦予後立刻會銷毀 3. 這是為了測試,**證明++暫時物件 (19.6) 會複製到原本物件 s++,這種過程稱為==自動轉換==** > 原本賦予值得 value = 100,暫時物件初始化 value = 0,暫時物件呼叫 `stonewt(double lbs)` 建構函數,只會複製其他成員,而沒有複製 value 4. 使用 `=` 會自動判斷類別型態,呼叫對應的建構函數,而 **double 就是類別型態** **--實作結果--** > 從結果也可以看到 **暫時物件會先被銷毀** > > ![](https://i.imgur.com/RXGTeyi.png) ### explicit 顯式型態 * **explicit 可以==關閉自動轉換==**,也就是關閉自動透過型態搜尋建構函數的功能 :::warning **`explicit` 必須定義在 class 宣告中,不可用於定義中** ::: * function 傳值會產生暫時物件,它會呼叫**複製建構函數** ```cpp= #include "iostream" class stonewt { private: static const int Lbs_per_stn = 14; // 石 : 磅 = 1 : 14 int stone; double p_lbs; // point lbs double t_lbs; // total lbs public: stonewt(); explicit stonewt(double lbs); // 磅; 在宣告中使用 `explicit`,禁止自動轉換 stonewt(int stone, double lbs); // 石 + 磅 ~stonewt(); void Show_Lbs() const; void Show_Stone() const; }; ``` > ![](https://i.imgur.com/Ngti9wK.png) ### 函數 - 暫時物件 * **暫時物件是==隱式轉換的一個中間產物==**,在 function 中同樣可以使用 ```cpp= class stonewt { ... public: ... void Show_Stone() const; // 新增 } void stonewt::Show_Stone() const { std::cout << "total stone: " << stone << "." << p_lbs << std::endl; } // --------------------------------------------------------------------- /** * 使用者 */ #include "./typeChange/stonewt.h" using std::cout; using std::endl; void display_1(const stonewt &s); void display_2(const stonewt s); int main() { cout << "\n display_1 只使用引數傳入" << endl; display_1(422.3); //"1. " cout << "\n display_2 只使用引數傳入" << endl; display_2(422.3); return 0; } void display_1(const stonewt &s) { //"2. " s.Show_Stone(); } void display_2(const stonewt s) { s.Show_Stone(); } ``` 1. `display_1` & `display_2` 的第一個引數是 stonewt 物件,遇到 **double 的引數傳入時編譯器會 ==++自動尋找++配對的上的建構函數==**,並呼叫對應的建構函數 (如上一個小節所說) 2. 接收 reference,使用原物件 (以目前來講就是使用 `暫時物件` 的引用) > ![](https://i.imgur.com/l0Z41AU.png) ### 反轉函數 - conversion function * 一般來說我們會把數值轉換為物件,但相同 **的物件也可以反轉為另一個使用者需要的值**;**C++ 的運算子函數格式稱為 ==反轉函數==** ```cpp= stonewt s1(275.3); // 一般使用,將 275.3 轉換為 stonewt 物件 double d = s1; // 反轉 ``` * 函數格式 : **==operator <類型>()==;++<類型> 代表了轉換的目標型態++**,並且轉換函數遵循以下條件 1. 必須是 **類別成員** 2. 不能指定回傳值 (有一定的格式) 3. 不能有引數 ```cpp= #include <iostream> //using namespace std; using std::cout; using std::endl; class MyClass { private: int studentCount; public: MyClass(int count = 0); ~MyClass(); // 反轉函數 operator int(); operator std::string(); }; MyClass::MyClass(int count) : studentCount(count) { cout << "MyClass construct." << endl; } MyClass::operator int() { return studentCount; } MyClass::operator std::string() { return std::to_string(studentCount); } MyClass::~MyClass() { cout << "MyClass deconstruct." << endl; } int main() { MyClass clz = 10; int val = clz; // 呼叫反轉函數 cout << "convert int: " << val << endl; std::string valStr = clz; // 呼叫反轉函數 cout << "convert string: " << valStr << endl; return 0; } ``` > ![](https://i.imgur.com/XK5ZLX4.png) * 反轉函數的使用又分為,**^1.^ ++隱性指定(implicit)++、^2.^ ++明確指定(explicit)++** * ==**隱性指定**==:**指定++反轉函數 operator int/double 類型++** ```cpp= int i1 = s1; double d1 = s1; ``` * ==**顯示指定**==:呼叫目標對象 ```cpp= int i2 = int(s1); double d2 = (double)s1; ``` ```cpp= /** * class 宣告 */ class stonewt { private: static const int Lbs_per_stn = 14; // 石 : 磅 = 1 : 14 int stone; double p_lbs; // point lbs double t_lbs; // total lbs public: stonewt(); stonewt(double lbs); // 磅 stonewt(int stone, double lbs); // 石 + 磅 ~stonewt(); stonewt(int v); void Show_Lbs() const; void Show_Stone() const; // convert func "1. " operator int() const; // const 可有可無 operator double() const; explicit operator float() const; // "3." 關閉隱式轉換 }; /** * class 定義 */ stonewt::operator int() const { return int(t_lbs + 0.5); // 四捨五入 } stonewt::operator double() const { return t_lbs; } stonewt::operator float() const { return t_lbs; } /** * 客戶端 使用 */ #include "./typeChange/stonewt.h" using std::cout; using std::endl; int main() { stonewt s1(275.3); s1.Show_Lbs(); // 隱式指定 int i1 = s1; cout << "int i1 = s1 : " << i1 << endl; double d1 = s1; cout << "double d1 = s1 : " << d1 << endl; int i2 = int(s1); cout << "int i2 = int(s1) : " << i2 << endl; double d2 = double(s1); cout << "double d2 = double(s1) : " << d2 << endl; double d3 = int(s1); cout << "double d3 = int(s1) : " << d3 << endl; float f1 = float(s1); cout << "float f1 = float(s1) : " << f1 << endl; //float f2 = s1; //"3. " can't implicit change, because use explicit return 0; } ``` 1. **反轉函數++沒有回傳值++、沒有引數,如果有使用 const function 描述會讓使用者使用的更加放心** 2. 顯式指定,由使用者規定自己需要的轉型,可規定轉型 int,但用 double 參數接收,結果會是 operator int funtion 的操作結果 3. 可以**使用 explicit 關閉隱式自動轉換** **--實做--** > ![](https://i.imgur.com/zhOfCGC.png) ### 成員函數 & friend 夥伴函數 * `成員函數`、`friend` 兩種方法都可重新定義符號操作 ```cpp= /** * 宣告 */ class stonewt { private: ... public: ... //member const stonewt operator+(const stonewt& s) const; // friend friend const stonewt operator+(const stonewt& s, const stonewt& s2); // 非成員函數不可使用 const }; /** * 定義新增 */ // member const stonewt stonewt::operator+(const stonewt& s) const { std::cout<< "member..." << std::endl; stonewt newS = s.t_lbs + this->t_lbs; return newS; } // friend const stonewt operator+(const stonewt& s1, const stonewt& s2) { std::cout<< "friend..." << std::endl; stonewt newS = s1.t_lbs + s2.t_lbs; return newS; } ``` * 上面定義兩個相同功能的函數,**==功能相同導致 candidate (候選) 發生==**,**會產生++混淆錯亂++** > ![](https://i.imgur.com/QCAkgt8.png) ## Appendix & FAQ :::info ::: ###### tags: `C++` `friend & operator` `explicit`