重載操作符與轉換--輸入/輸出、算術/關係操作符
支援I/O操作的類所提供的I/O操作介面,一般應該與標準庫iostream為內建類型定義的介面相同,因此,許多類都需要重載輸入和輸出操作符。
一、輸出操作符<<的重載
為了與IO標準庫一致,操作符應接受ostream&作為第一個形參,對類類型const對象的引用作為第二個形參,並返回ostream形參的引用!
ostream &operator<<(ostream &os,const ClassType &object){ os << //.... return os;}
1、Sales_item輸出操作符
ostream &operator<<(ostream &out,const Sales_item &object){ out << object.isbn << '\t' << object.units_sold << '\t' << object.revenue << '\t' << object.avg_price(); return out;}
2、輸出操作符通常所做格式化應盡量少
一般而言,輸出操作符應輸出對象的內容,進行最小限度的格式化,它們不應該輸出分行符號!盡量減少操作符的格式化,可以讓使用者自己控制輸出細節。
Sales_item item("C++ Primer"); cout << item << endl;//使用者自己控制輸出分行符號
3、IO操作符必須為非成員函數
我們不能將該操作符定義為類的成員,否則,左操作符只能是該類型的對象:
ostream &Sales_item::operator<<(ostream &out){ out << isbn << '\t' << units_sold << '\t' << revenue << '\t' << avg_price(); return out;}
//測試 Sales_item item("C++ Primer"); //這個用法與正常使用方式恰好相反 item << cout << endl; //OR item.operator<<(cout); //Error cout << item << endl;
如果想要支援正常用法,則左運算元必須為ostream類型。這意味著,如果該操作符是類的成員,則它必須是ostream類的成員,然而,ostream類是標準庫的組成部分,我們(以及任何想要定義IO操作符的人)是不能為標準庫中的類增加成員的。
由於IO操作符通常對非公用資料成員進行讀寫,因此,類通常將IO操作符設為友元。
//P437 習題14.7class CheckoutRecord{ friend ostream &operator<<(ostream &os,const CheckoutRecord &object);public: typedef unsigned Date; //...private: double book_id; string title; Date date_borrowed; Date date_due; pair<string,string> borrower; vector< pair<string,string> * > wait_list;};ostream &operator<<(ostream &os,const CheckoutRecord &obj){ os << obj.book_id << ": " << obj.title << '\t' << obj.date_borrowed << '\t' << obj.date_due << '\t' << obj.borrower.first << ' ' << obj.borrower.second << endl; os << "Wait_list:" << endl; for (vector< pair<string,string> * >::const_iterator iter = obj.wait_list.begin(); iter != obj.wait_list.end(); ++iter) { os << (*iter) -> first << '\t' << (*iter) -> second << endl; }}
二、輸入操作符>>的重載
與輸出操作符類似,輸入操作符的第一個形參是一個引用,指向它要讀的流,並且返回的也是對同一個流的引用。它的第二個形參是對要讀入的對象的非const引用,該形參必須為非const,因為輸入操作符的目的是將資料讀到這個對象中。
輸入操作符必須處理錯誤和檔案結束的可能性!
1、Sales_item的輸入操作符
istream &operator>>(istream &in,Sales_item &s){ double price; in >> s.isbn >> s.units_sold >> price; if (in) { s.revenue = price * s.units_sold; } else { //如果讀入失敗,則將對象重新設定成為預設狀態 s = Sales_item(); } return in;}
2、輸入期間的錯誤
可能發生的錯誤包括:
1)任何讀操作都可能因為提供的值不正確而失敗。例如,讀入isbn之後,輸入操作符將期望下兩項是數值型資料。如果輸入非數值型資料,這次的讀入以及流的後續使用都將失敗。
2)任何讀入都可能碰到輸入資料流中的檔案結束或其他一些錯誤。
但是我們無需檢查每次讀入,只在使用讀入資料之前檢查一次即可。
if (in) { s.revenue = price * s.units_sold; } else { s = Sales_item(); }
如果一旦出現了錯誤,我們不用關心是哪個輸入失敗了,相反,我們將整個對象複位!
3、處理輸入錯誤
如果輸入操作符檢測到輸入失敗了,則確保對象處於可用和一致狀態是個好做法!如果對象在發生錯誤之前已經寫入了部分資訊,這樣做就特別重要!
例如,在Sales_item的輸入操作符中,可能成功地讀入了一個新的isbn,然後遇到流錯誤。在讀入isbn之後發生錯誤意味著舊對象的units_sold和 revenue成員沒變,結果會將另一個isbn與那個資料關聯(悲劇了...)。因此,將形參恢複為空白Sales_item對象,可以避免給他一個無效的狀態!
【最佳實務】
設計輸入操作符時,如果可能,要確定錯誤恢複措施,這很重要!
4、指出錯誤
除了處理可能發生的任何錯誤之外,輸入操作符還可能需要設定輸入形參的條件狀態。
有些輸入操作符的確需要進行附加檢查。例如,我們的輸入操作符可以檢查讀到的 isbn格式是否恰當。也許我們已成功讀取了資料,但這些資料不能恰當解釋為ISBN,在這種情況下,儘管從技術上說實際的IO是成功的,但輸入操作符仍可能需要設定條件狀態以指出失敗。通常輸入操作符僅需設定failbit。設定 eofbit意思是檔案耗盡,設定badbit可以指出流被破壞,這些錯誤最好留給 IO標準庫自己來指出。
//P439 習題14.11class CheckoutRecord{ friend istream &operator>>(istream &in,CheckoutRecord &object);public: typedef unsigned Date; //...private: double book_id; string title; Date date_borrowed; Date date_due; pair<string,string> borrower; vector< pair<string,string> * > wait_list;};istream &operator>>(istream &in,CheckoutRecord &obj){ in >> obj.book_id >> obj.title >> obj.date_borrowed >> obj.date_due; in >> obj.borrower.first >> obj.borrower.second; if (!in) { obj = CheckoutRecord(); return in; } obj.wait_list.clear(); while (in) { pair<string,string> *p = new pair<string,string>; in >> p -> first >> p -> second; if (in) { obj.wait_list.push_back(p); delete p; } } return in;}
三、算術運算子
一般而言,將算術和關係操作符定義為非成員函數:
Sales_item operator+(const Sales_item &lhs,const Sales_item &rhs){ Sales_item ret(lhs); //使用Sales_item的複合複製操作符來加入rhs的值 ret += rhs; return ret;}
加法操作符並不改變運算元的狀態,運算元是對const對象的引用。
【最佳實務】
為了與內建操作符保持一致,加法返回一個右值,而不是一個引用!
既定義了算術操作符又定義了先關複合賦值操作符的類,一般應使用複合賦值實現算術操作符。
//P440 習題14.12Sales_item operator+(const Sales_item &lhs,const Sales_item &rhs){ Sales_item tmp; tmp.units_sold = lhs.units_sold + rhs.units_sold; tmp.revenue = lhs.revenue + rhs.revenue; return tmp;}Sales_item& Sales_item::operator+=(const Sales_item& rhs){ *this = *this + rhs; return *this;}
四、關係運算子
1、相等運算子
如果所有對應成員都相等,則認為兩個對象相等。
inlinebool operator==(const Sales_item &lhs,const Sales_item &rhs){ return lhs.revenue == rhs.revenue && lhs.units_sold == rhs.units_sold && lhs.same_isbn(rhs);}inlinebool operator!=(const Sales_item &lhs,const Sales_item &rhs){ return !(lhs == rhs);}
1)如果類定義了==操作符,該操作符的含義是兩個對象包含同樣的資料。
2)如果類具有一個操作,能確定該類型的兩個對象是否相等,通常將該函數定義為 operator==而不是創造命名函數。使用者將習慣於用==來比較對象,而且這樣做比記住新名字更容易。
3)如果類定義了operator==,它也應該定義operator!=。使用者會期待如果可以用某個操作符,則另一個也存在。
4)相等和不操作符一般應該相互聯絡起來定義,讓一個操作符完成比較對象的實際工作,而另一個操作符只是調用前者。
定義了operator==的類更容易與標準庫一起使用。有些演算法,如find,預設使用==操作符,如果類定義了==,則這些演算法可以無需任何特殊處理而用於該類類型!
2、關係操作符
定義了相等操作符的類一般也具有關係操作符。尤其是,因為關聯容器和某些演算法使用小於操作符(<),所以定義了operator<可能相當有用。
如果因為<的邏輯定義與==的邏輯定義不一致,所以這樣的話,不定義<會更好。
【注釋】
關聯容器以及某些演算法,預設使用<操作符(此處本人認為譯者翻譯有誤,原文:...usethe < operator bydefult...,譯者翻譯為:使用預設<操作符,但本人認為預設使用更為恰當!)。一般而言,關係操作符,諸如相等操作符,應定義為非成員函數(“對稱”操作符)。