Try   HackMD
tags: 大一程設-下 東華大學 東華大學資管系 基本程式概念 資管經驗分享

Operator Overloading 多載運算子

前言

多載運算子的核心概念 : 自己定義運算子的行為
簡化程式複雜性,增加閱讀性。
以使用者的角度,他不會知道背後實作的細節。(這句話是重點!!)
Orange

什麼叫做自己定義運算子的行為?

應該都記得運算子是什麼,+-*/++、-->>、<<==、= 之類的很多運算子。

以四則運算的 + 號為例,如果執行 5 + 10,照常會把兩個數字做相加,而今天自定義 + 號的意思就是,讓他執行我們自己設定的行為。比方說 5 + 10 透過我們自定義之後,會執行 5 * 2 + 10 * 2。這邊就是說每個數字先 * 2 再相加這件事是我們自己定義的。

還是不太懂吧,我們繼續往下,但這樣的概念你們可以先有。

為何要 operator overloading?

不失一般性,我還是想先讓大家知道為甚麼要做這件事。

我們假設一個情境,假設今天老師需要一個計算全班每個學生五個考科成績平均的簡單介面,
而這段程式會由 C++ 開發,我們可能可以有下面這樣的類別。

tip: 面對這樣的題目或敘述,請先確定需求!

  • 需求
    • 要能夠記錄每個學生各自的五科成績
    • 要能夠算總和
    • 要能夠算平均
    • 要能夠顯示平均與各科成績

如果今天沒有 operator overloading 我們可能會實作出下面的類別。
範例很長,下面會分段說明。

class student{ public: student() : name(""), chinese(0), english(0), math(0), society(0), science(0), average(0){} void input_name(){ cout << "input student name..." << endl; cin >> name; } void input_score(){ cout << "plz input chinese, english, math, society, science..." << endl; cin >> chinese >> english >> math >> society >> science; } string get_average(){ average = (chinese + english + math + society + science) / 5; return name + "\'s average is " + to_string(this->average); } string get_sum(){ sum = chinese + english + math + society + science; return name + "\'s sum is " + to_string(sum); } void get_score(){ cout << "grade of " << name << endl << "--------------" << endl; cout << "chinese: " << chinese << endl << "english: " << english << endl << "math: " << math << endl << "society: " << society << endl << "science: " << science << endl << "average: " << average << endl << "sum: " << sum << endl << "--------------" << endl; } private: string name; double chinese; double english; double math; double society; double science; double average; double sum; }; int main(){ student s1; s1.input_name(); s1.input_score(); cout << s1.get_average() << endl; cout << s1.get_sum() << endl; s1.get_score(); return 0; }

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

接下來我會以開發者的角度解釋為甚麼要使用 operator overloading

對於開發者而言

可以看看上面的例子,為了輸入名字、輸入成績、算平均、算總和、取得成績,我們實作了非常多的 member function,你可以發現為了一件小事情,為了劃分功能我們可能會拆成 function 出去,這件事情本身沒有問題,邏輯很正確。也確實得到了我們的效果。

但這樣的問題是,一個小功能我們就需要一個 function,隨著功能越來越多,我們會宣告更多的函式,對於開發而言,其實有點沒有效率。

而你也可以看到上例需要非常多的,輸入、取值的動作,這個時候如果自定義運算子來執行我們的行為,我們可以簡化非常多的程式。

語法認識 operator overloading / overloading with friend

為了搭配老師用的教科書,我們先說明結合 friend 的運算子多載。
此處不再說明何為 friend,忘記請回去看前面的筆記。

operator overloading with friend 語法

// 多載二元運算子 friend return_type operator 要多載的運算子(參數1, 參數2){ /* 函式內容 */ } // 多載一元運算子 friend return_type operator 要多載的運算子(參數1){ /* 函式內容 */ }

舉例

// overloading 二元 + 運算子,回傳值是 double friend double operator + (student& s1, student& s2){} // overloading 一元 + 運算子,回傳值是 double friend double operator + (student& s1){} // overloading 二元 >> 運算子 // 我們自己來定義輸入 friend istream& operator >> (istream&in, student& s){}

參數如何傳遞

了解多載運算子的參數傳遞之前,我們在每次要多載的時候,要先知道我們要多載的運算子是幾元的運算子。比方說多載 +-*/ 是二元運算子,>> << 是二元運算子,但是 +-,也能夠當作一元運算子來使用。沒有多載的時候他們是表達正負號。

所以根據你要多載的運算子是幾元運算子,就會需要幾個參數
而運算子的運作方式則不會改變,多載二元運算子的加號一定要寫 變數1 + 變數2。依此類推。

而根據你的參數的型態,你必須用相對應型態的變數來使用多載的運算子,以下舉例。

/* 類別詳細內容此處忽略 */ // 實作多載 friend double operator +(student& s1, student& s2){ return (s1.score + s2.score); } int main(){ // 使用多載運算子 student s1, s2; cout << s1 + s2; }

可以看到上面這段程式,我們實作了多載 + 運算子,其有兩個參數,都是 student 物件,所以今天若是要呼叫自定義的多載運算子,必須確實使用兩個 student 物件來呼叫他。
而上面這段多載運算子是在執行把兩個當作參數傳入的學生的成績做相加。

到這邊都理解的話,我們直接看下方的大例子,直接修改上方的例子。

範例很長,下面會分段說明。

class student{ public: // constructor student() : name(""), chinese(0), english(0), math(0), society(0), science(0), average(0){} // getter string get_average(){ return name + "\'s average is " + to_string(this->average); } // operator overloading friend istream& operator >> (istream& in, student& s); friend ostream& operator << (ostream& out, const student& s); private: string name; double chinese; double english; double math; double society; double science; double average; double sum; }; ostream& operator << (ostream& out, const student& s){ cout << "grade of " << s.name << endl << "--------------" << endl; cout << "chinese: " << s.chinese << endl << "english: " << s.english << endl << "math: " << s.math << endl << "society: " << s.society << endl << "science: " << s.science << endl << "average: " << s.average << endl << "sum: " << s.sum << endl << "--------------" << endl; return out; } istream& operator >> (istream& in, student& s){ cout << "input student name..." << endl; in >> s.name; cout << "plz input chinese, english, math, society, science..." << endl; in >> s.chinese >> s.english >> s.math >> s.society >> s.science; s.sum = s.chinese + s.english + s.math + s.society + s.science; s.average = (s.chinese + s.english + s.math + s.society + s.science)/5; return in; } int main(){ double sum_score; student s1; cin >> s1; cout << s1; cout << s1.get_average(); return 0; }

小總結

可以上下比對兩個例子,main function 的簡潔度差非常多。類別本身的定義也簡潔很多。

但你可能會說,實際比較下來不就是把 function 的內容全部都實作到 operator overloading 裡嗎? 看起來其實也沒有比較方便。

直觀理解下來是這樣沒有錯,但其實實作 operator overloading 自定義運算子之後,我們能夠結合 friend 憑藉參數來取用私有屬性,而且可以自定義我們想要的行為,如果今天仍是使用成員函式,我們的行為會被綁定,因為 function 要在類別內先被定義好。

那你會說,我就想要宣告一堆 function 在 class 裡阿。

沒有不行,完全可以
Orange

這邊不考慮抽象方法之類的內容。
歡迎自行上網學習!
Orange

所以究竟要不要使用 operator overloading 是看需求的,他確實提供了一個額外的選項讓我們實作程式,讓我們有更多工具把玩程式。學起來有利無弊。

程式碼解釋

// 多載 >> 運算子,因為為二元,需有兩個參數 friend istream& operator >> (istream& in, student& s){ /* 多載內容 */ } // 多載 << 運算子,因為為二元,需有兩個參數 friend ostream& operator << (ostream& out, const student& s){ /* 多載內容 */ } int main(){ // 使用多載運算子 student s1; cin >> s1; cout << s1; }

相信 istreamostream 對第一次見到的你們肯定不陌生(?。
還記得在第六章有 ifstreamofstream 兩個類別,全名 input file stream/ output file stream,負責檔案讀入與寫出串流。
istream/ostream 則是負責使用者輸入與輸出的串流的類別。

這裡我們自定義了 >> 運算子,而第一個參數必須要是輸入串流物件,第二個參數是一個學生物件,最後會回傳一個輸入串流物件,所以為了滿足行為,可以看上方第 13 行,

cin 滿足第一個參數,s1 滿足第二個參數,根據我們自定義了 >> 的行為,參數符合呼叫的規定,所以這邊的 cin >> s1 會執行我們自定義的 operator overloading。

第 14 行的自定義 << 也同理,可以自己理解一下。

有關於 istream 與 ostream 的回傳

// 上方類別的內容與前例相同,此處忽略不寫 int main(){ // 假設有三名學生的資訊需要輸入 student s1, s2 ,s3; cin >> s1 >> s2 >> s3; cout << s1 << s2 << s3; return 0; }

上方例子的第五行與第六行做了連續的輸入動作,因為參數符合自定義運算子的呼叫,所以執行 operator overloading。

還記得運算子的優先權,>>、<< 這兩個運算子是左結合,所以左邊優先,實際運作起來會像下面這樣

(((cin >> s1) >> s2) >> s3); (((cout << s1) << s2) << s3);

而每一次輸入都會回傳一個讀入串流物件(根據我們上面自定義的運算子),所以 cin >> s1 執行完之後會把讀入串流 cin 回傳回來,再執行 cin >> s2,依此類推。

自定義的注意事項

  • 若自定義時會運用到串流物件,請一定要 call-by-reference,回傳 type 也一定要為參考變數
    • 因為程式一次只會有一個讀入或寫出串流在運作,其實整份程式只需要一個讀入或串流物件,所以只需要去取用那份記憶體即可
  • 當自定義運算子時,至少一個參數要是該類別物件,否則沒有自定義的必要
  • N 元運算子,就算你要 overloading,他也還是 N 元運算子
    • 以二元 + 號為例,若要 overloading,其所需參數就一定是兩個,不可能三個
  • 運算子的優先權與結合性是不可能被改變的,就算你要 overloading 他也還是會遵循他基本的特性
  • 以下的運算子沒有辦法被 overloading
    • .
    • ::

operator overloading without friend

今天運算子多載若要宣告成 member function 也是完全沒問題的,但這樣要呼叫時就必須要由一個實體化的物件親自呼叫來觸發。

可以看底下 Reference 的第一篇。

上面有關 friend 的都看懂了的話沒 friend 的你一定也能夠心領神會了
我有空會再回過頭來補
Orange

總結

究竟 operator overloading 要不要使其為 friend function,要依照你的需求,實作的 function 能不能存取私有屬性或方法來決定,所以沒有一定。

Reference