文章目錄
- 輸出運算符<<
- 輸入運算符>>
- 相等/不等運算符
- 復合賦值運算符
- 下標運算符
- 自增/自減運算符
- 成員訪問運算符
輸出運算符<<
通常情況下,輸出運算符的第一個形參是一個 非常量ostream對象的引用
。之所以 ostream
是非常量是因為向流寫入內容會改變其狀態;而該形參是引用是因為我們無法直接復制一個 ostream
對象。
第二個形參一般來說是一個 常量的引用,該常量是我們想要打印的類類型。第二個形參是引用的原因是我們希望避免復制實參;而之所以該形參可以是常量是因為(通常情況下)打印對象不會改變對象的內容。
為了與其他輸出運算符保持一致,operator<<
一般要返回它的 ostream
形參。
通常我們需要在類中重載 <<
以避免查看成員時輸出操作過于繁瑣:
class A {friend ostream& operator<<(ostream& os, const A& a);int i = 1;double d = 3.14;
};
ostream& operator<<(ostream& os, const A& a) {os << a.i << " " << a.d;return os;
}
值得注意的幾點:
- 減少格式化操作(如:換行符): 目的是給用戶更大的自由去決定輸出的格式,如果我們自帶換行,那么用戶就無法在同一行內解接著打印一些描述性文本了。
- 輸入輸出運算符必須是非成員函數: 如果是某個類的成員函數,則輸入輸出運算符也必須是
istream
或ostream
成員(詳見下文),但是這兩個類(istream
、ostream
)屬于標準庫,而我們無法給標準庫中的類添加任何成員。 - 可以將IO運算符聲明為友元: 既然我們的
IO操作
又想訪問類的私有成員,又不能是類的成員函數,那么聲明成友元是最佳選擇。
用例子來解釋一下第二點:
我們都知道重載運算符的返回類型一定要與它的實際操作相匹配,因此,重載 ==
返回值為 bool
;重載 +
返回值為 類的引用
……
輸入輸出運算符是 IO類
的成員函數,因此其返回類型是 IO類本身
,那么如果某個類將 重載的輸入輸出運算符 作為 成員函數 的話,返回類型 就會變成 這個類本身,重載的輸入輸出運算符的左側運算對象則是這個類的一個對象:
class B {int i = 1;double d = 3.14;
public:ostream& operator<<(ostream& os) {os << i << " " << d;return os;}
};
B b;
b << cout; // 這樣調用不符合我們的輸出習慣
如此一來改變了 <<
的調用方式,也就不算構成重載了,如果既要 cout << b;
,還要 <<
是 B
的成員。那么就要在 ostream類
中添加 ostream& operator<<(B&);
,可正如前文所說,ostream
屬于標準庫,我們無法給標準庫中的類添加任何成員。ostream
中 <<
的各類重載如下:
輸入運算符>>
- 通常情況下,輸入運算符的第一個形參是運算符將要讀取的流的引用
- 第二個形參是將要讀入到的對象的引用(對象不能是常量,因為將數據讀入到這個對象中實際是修改了這個對象)
- 通常會返回某個給定流的引用
與輸出運算符不同的是,輸入運算符必須處理輸入失敗的情況:
class A {friend istream& operator>>(istream& is, A& a);friend ostream& operator<<(ostream& os, const A& a);int i = 1;double d = 3.14;
};istream& operator>>(istream& is, A& a) {int i1;double d1;is >> i1 >> d1;if (is) {a.i = i1;a.d = d1;}else a = A();return is;
}ostream& operator<<(ostream& os, const A& a) {os << a.i << " " << a.d;return os;
}
重寫輸入運算符后,可以對輸入的數據進行對應處理:
當沒有輸入/輸入錯誤時,用構造函數創建一個臨時量,然后調用賦值運算符為 a
賦值:
當有多個輸入時,對輸入進行處理:
相等/不等運算符
對于類而言,判斷相等需要比較每一項數據成員,因此有必要對相等運算符進行重載。
如果定義了 operator==
,則這個類也應該定義 operator!=
。對于用戶來說,當他們能使用 ==
時肯定也希望能使用 !=
,反之亦然。
相等運算符和不相等運算符中的一個應該把工作委托給另外一個,這意味著其中一個運算符應該負責實際比較對象的工作,而另一個運算符則只是調用那個真正工作的運算符。
class A {friend istream& operator>>(istream& is, A& a);friend ostream& operator<<(ostream& os, const A& a);int i = 1;double d = 3.14;
public:bool operator==(const A& a) {return this->d == a.d && this->i == a.i;}bool operator!=(const A& a) {return !(*this == a);}
};
復合賦值運算符
復合賦值運算符不一定非得是類的成員,不過我們還是傾向于把包括復合賦值在內的所有賦值運算都定義在類的內部。為了與內置類型的復合賦值保持一致,類中的復合賦值運算符也要返回其左側運算對象的引用。
PS:賦值運算符必須是類的成員
A& operator+=(const A& a) {i += a.i;d += a.d;return *this;
}
下標運算符
- 下標運算符必須是成員函數。
- 為了與下標的原始定義兼容,下標運算符通常以所訪問元素的引用作為返回值,這樣做的好處是下標可以出現在賦值運算符的任意一端。
- 如果一個類包含下標運算符,則它通常會定義兩個版本:一個返回普通引用,另一個是類的常量成員并且返回常量引用。
class IntVec // IntVec是對標準庫vector類的模仿,僅存儲int元素
{int* begin; // 指向已分配的內存中的首元素int* end; // 指向最后一個實際元素之后的位置int* cap; // 指向分配的內存末尾之后的位置
public:int& operator[](int n) { return begin[n]; }const int& operator[](int n) const { return begin[n]; } // 第二個const修飾*this
};
下標運算符返回的是元素的引用,當 IntVec
是非常量時,我們可以給元素賦值;而我們對常量對象取下標時,不能對其賦值。
自增/自減運算符
與內置類型一樣,重載的自增自減同時要有前置版本和后置版本。
要想同時定義前置和后置運算符,必須首先解決一個問題,即普通的重載形式無法區分這兩種情況。
為了解決這個問題,后置版本接受一個額外的(不被使用)int
類型的形參。當我們使用后置運算符時,編譯器為這個形參提供一個值為 0
的實參。盡管從語法上來說后置函數可以使用這個額外的形參,但是在實際過程中通常不會這么做。這個形參的唯一作用就是區分前置版本和后置版本的函數,而不是真的要在實現后置版本時參與運算。
前置版本:
// 僅作偽代碼實現
類名& operator++();
類名& operator--();
后置版本:
為了與內置版本保持一致,后置運算符應該返回對象的原值(遞增或遞減之前的值),返回的形式是一個值而非引用。
類名 operator++(int); // 我們不會用到int形參,因此無需為它命名。
類名 operator--(int);
// 舉個例子,但不詳細實現Ptr類了,可以將它理解為 IntVec(或真正的順序容器) 的指針類
Ptr operator++(int){Ptr ret = *this; // 記錄當前值++*this; // 調用前置++運算符,前置++需要檢查自增的有效性return res; // 返回之前記錄的狀態
}
/* 顯式地調用后置運算符 */
Ptr p(v); // p指向v中的vector
p.operator++(0); // 調用后置版本,盡管0會被忽略,卻必不可少,因為編譯器只有通過它才知道應該使用后置版本。
p.operator++(); // 調用前置版本
成員訪問運算符
箭頭運算符(->
)必須是類的成員。 解引用運算符(*
)則無硬性要求。
// 偽代碼
class Ptr{
public:int& operator*() const {// 檢查解引用對象是否在規定范圍內return *p[下標]; // *p可以是形如vector的對象}int* operator->() const {return & this->operator*(); //將工作委托給解引用運算符}
};
較之解引用運算符,重載箭頭運算符有些限制,重載的箭頭運算符必須返回類的指針或者自定義了箭頭運算符的某個類的對象。
例如,對于形如 p->mem
的表達式來說,根據 p
類型的不同,表達式分別等價于:
(*p).mem; // p是一個內置的指針類型
p.operator()->mem; // p是類的對象
除此之外,代碼都將發生錯誤。p->mem
的執行過程如下所示:
1.如果 p
是指針,則我們應用內置的箭頭運算符,首先解引用該指針,然后從所得的對象中獲取指定的成員。如果 p
所指的類型沒有名為 mem
的成員,程序會發生錯誤。
2.如果 p
是定義了 operator->
的類的一個對象,如果 p.operator()->
的結果是一個指針,則執行第1步;如果該結果本身含有重載的 operator->()
,則重復調用當前步驟。最終,當這一過程結束時程序或者返回了所需的內容,或者返回一些表示程序錯誤的信息。