# [13] 運算符重載
## FAQs in section [13]:
* [13.1] 運算符重載的作用是什么?
* [13.2] 運算符重載的好處是什么?
* [13.3] 有什么運算符重載的實例?
* [13.4] 但是運算符重載使得我的類很丑陋;難道它不是應該使我的類更清晰嗎?
* [13.5] 什么運算符能/不能被重載?
* [13.6] 我能重載 `operator==` 以便比較兩個 `char[]` 來進行字符串比較嗎?
* [13.7] 我能為“冪”運算創建一個 `operator**` 嗎?
* [13.8] 如何為`Matrix`(矩陣)類創建下標運 運算符?
* [13.9] 為什么`Matrix`(矩陣)類的接口不應該象數組的數組?
* [13.10] 該從外(接口優先)還是從內(數據優先)設計類?
## 13.1 運算符重載的作用是什么?
它允許你為類的用戶提供一個直覺的接口。
運算符重載允許C/C++的運算符在用戶定義類型(類)上擁有一個用戶定義的意義。重載的運算符是函數調用的語法修飾:
```
?class?Fred?{
?public:
???//?...
?};
?#if?0
???//?沒有運算符重載:
???Fred?add(Fred,?Fred);
???Fred?mul(Fred,?Fred);
???Fred?f(Fred?a,?Fred?b,?Fred?c)
???{
?????return?add(add(mul(a,b),?mul(b,c)),?mul(c,a));????//?哈哈,多可笑...
???}
?#else
???//?有運算符重載:
???Fred?operator+?(Fred,?Fred);
???Fred?operator*?(Fred,?Fred);
???Fred?f(Fred?a,?Fred?b,?Fred?c)
???{
?????return?a*b?+?b*c?+?c*a;
???}
?#endif
```
## 13.2 運算符重載的好處是什么?
通過重載類上的標準運算符,你可以發掘類的用戶的直覺。使得用戶程序所用的語言是面向問題的,而不是面向機器的。
最終目標是降低學習曲線并減少錯誤率。
## 13.3 有什么運算符重載的實例?
這里有一些運算符重載的實例:
* `myString?+?yourString` 可以連接兩個 `std::string` 對象
* `myDate++` 可以增加一個 `Date` 對象
* `a?*?b` 可以將兩個 `Number` 對象相乘
* `a[i]` 可以訪問 `Array` 對象的某個元素
* `x?=?*p` 可以反引用一個實際“指向”一個磁盤記錄的 "smart pointer" —— 它實際上在磁盤上定位到 `p` 所指向的記錄并返回給`x`。?
## 13.4 但是運算符重載使得我的類很丑陋;難道它不是應該使我的類更清晰嗎?
運算符重載使得類的用戶的工作更簡易,而不是為類的開發者服務的!
考慮一下如下的例子:
```
?class?Array?{
?public:
???int&?operator[]?(unsigned?i);??????//?有些人不喜歡這種語法
//?...
?};
?inline
?int&?Array::operator[]?(unsigned?i)??//?有些人不喜歡這種語法
?{
???//?...
?}
```
有些人不喜歡`operator`關鍵字或類體內的有些古怪的語法。但是運算符重載語法不是被期望用來使得類的開發者的工作更簡易。它被期望用來使得類的用戶的工作更簡易:
```
?int?main()
?{
???Array?a;
???a[3]?=?4;???//?用戶代碼應該明顯而且易懂...
?}
```
記住:在一個面向重用的世界中,使用你的類的人有很多,而建造它的人只有一個(你自己);因此你做任何事都應該照顧多數而不是少數。
## 13.5 什么運算符能/不能被重載?
大多數都可以被重載。C的運算符中只有 `.`和 `? :`(以及`sizeof`,技術上可以看作一個運算符)。C++增加了一些自己的運算符,除了`::`和`.*`,大多數都可以被重載。
這是一個下標 運算符的示例(它返回一個引用)。先沒有運算符重載:
```
?class?Array?{
?public:
???int&?elem(unsigned?i)????????{?if?(i?>?99)?error();?return?data[i];?}
?private:
???int?data[100];
?};
?int?main()
?{
???Array?a;
???a.elem(10)?=?42;
???a.elem(12)?+=?a.elem(13);
?}
```
現在用運算符重載給出同樣的邏輯:
```
?class?Array?{
?public:
???int&?operator[]?(unsigned?i)?{?if?(i?>?99)?error();?return?data[i];?}
?private:
???int?data[100];
?};
?int?main()
?{
???Array?a;
???a[10]?=?42;
???a[12]?+=?a[13];
?}
```
## 13.6 我能重載 `operator==` 以便比較兩個 `char[]` 來進行字符串比較嗎?
不行:被重載的運算符,至少一個操作數必須是用戶定義類型(大多數時候是類)。
但即使C++允許,也不要這樣做。因為在此處你應該使用類似 `std::string`的類而不是字符數組,因為數組是有害的。因此無論如何你都不會想那樣做的。
## 13.7 我能為“冪”運算創建一個 `operator**` 嗎?
不行。
運算符的名稱、優先級、結合性以及元數都是由語言固定的。在C++中沒有`operator**`,因此你不能為類類型創建它。
如果還有疑問,考慮一下`x?**?y`與`x?*?(*y)`等同(換句話說,編譯器假定 `y` 是一個指針)。此外,運算符重載只不過是函數調用的語法修飾。雖然這種特殊的語法修飾非常美妙,但它沒有增加任何本質的東西。我建議你重載`pow(base,exponent)`(雙精度版本在`<cmath>`中)。
順便提一下,`operator^`可以成為冪運算,只是優先級和結合性是錯誤的。
## 13.8 如何為`Matrix`(矩陣)類創建下標運算符?
用 `operator()`而不是`operator[]`。
當有多個下標時,最清晰的方式是使用`operator()`而不是`operator[]`。原因是`operator[]`總是帶一個參數,而`operator()`可以帶任何數目的參數(在矩形的矩陣情況下,需要兩個參數)。
如:
```
?class?Matrix?{
?public:
???Matrix(unsigned?rows,?unsigned?cols);
???double&?operator()?(unsigned?row,?unsigned?col);
???double??operator()?(unsigned?row,?unsigned?col)?const;
???_//?..._
??~Matrix();??????????????????????????????//?析構函數
???Matrix(const?Matrix&?m);???????????????//?拷貝構造函數
???Matrix&?operator=?(const?Matrix&?m);???//?賦值運算符
//?...
?private:
???unsigned?rows_,?cols_;
???double*?data_;
?};
?inline
?Matrix::Matrix(unsigned?rows,?unsigned?cols)
???:?rows_?(rows),
?????cols_?(cols),
?????data_?(new?double[rows?*?cols])
?{
???if?(rows?==?0?||?cols?==?0)
?????throw?BadIndex("Matrix?constructor?has?0?size");
?}
?inline
?Matrix::~Matrix()
?{
???delete[]?data_;
?}
?inline
?double&?Matrix::operator()?(unsigned?row,?unsigned?col)
?{
???if?(row?>=?rows_?||?col?>=?cols_)
?????throw?BadIndex("Matrix?subscript?out?of?bounds");
???return?data_[cols_*row?+?col];
?}
?inline
?double?Matrix::operator()?(unsigned?row,?unsigned?col)?const
?{
???if?(row?>=?rows_?||?col?>=?cols_)
?????throw?BadIndex("const?Matrix?subscript?out?of?bounds");
???return?data_[cols_*row?+?col];
?}
```
然后,你可以使用`m(i,j)`來訪問`Matrix` `m` 的元素,而不是`m[i][j]:`
```
?int?main()
?{
???Matrix?m(10,10);
???m(5,8)?=?106.15;
???std::cout?<<?m(5,8);
???//?...
?}
```
## 13.9 為什么`Matrix`(矩陣)類的接口不應該象數組的數組?
本 FAQ 其實是關于:某些人建立的Matrix 類,帶有一個返回 `Array` 對象的引用的`operator]`。而該`Array` 對象也帶有一個 `operator[]` ,它返回Matrix的一個元素(例如,一個`double`的引用)。因此,他們使用類似`m[i][j]` 的語法來訪問矩陣的元素,而不是[象`m(i,j)`的語法。
數組的數組方案顯然可以工作,但相對于`operator()`方法來說,缺乏靈活性。尤其是,用`[][]`方法很難表現的時候,用`operator()`方法可以很簡單的完成,因此`[][]`方法很可能導致差勁的表現,至少某些情況細是這樣的。
例如,實現`[][]`方法的最簡單途徑就是使用作為密集矩陣的,以以行為主的形式保存(或以列為主,我記不清了)的物理布局。相反,`operator()` 方法完全隱藏了矩陣的物理布局,在這種情況下,它可能帶來更好的表現。
可以這么認為:`operator()`方法永遠不比`[][]`方法差,有時更好。
* `operator()` 永遠不差,是因為用`operator()`方法實現以行為主的密集矩陣的物理布局非常容易。因此,當從性能觀點出發,那樣的結構正好是最佳布局時,`operator()`方法也和`[][]`方法一樣簡單(也許`operator()`方法更容易一點點,但我不想夸大其詞)。
* `operator()`方法有時更好,是因為當對于給定的應用,有其它比以行為主的密集矩陣更好的布局時,用 `operator()` 方法比`[][]`方法實現會容易得多。
作為一個物理布局使得實現困難的例子,最近的項目發生在以列訪問矩陣元素(也就是,算法訪問一列中的所有元素,然后是另一列等),如果物理布局是以行為主的,對矩陣的訪問可能會“cache失效”。例如,如果行的大小幾乎和處理器的cache大小相當,那么對每個元素的訪問,都會發生“cache不命中”。在這個特殊的項目中,我們通過將映射從邏輯布局(行,列)變為物理布局(列,行),性能得到了20%的提升。
當然,還有很多這類事情的例子,而稀疏矩陣在這個問題中則是又一類例子。通常,使用`operator()`方法實現一個稀疏矩陣或交換行/列順序更容易,`operator()`方法不會損失什么,而可能獲得一些東西——它不會更差,卻可能更好。
使用 `operator()` 方法。
## 13.10 該從外(接口優先)還是從內(數據優先)設計類?
從外部!
良好的接口提供了一個簡化的,以用戶詞匯表達的視圖。在面向對象軟件的情況下,接口通常是單個類或一組緊密結合的類的public方法的集合.
首先考慮對象的邏輯特征是什么,而不是打算如何創建它。例如,假設要創建一個`Stack`(棧)類,其包含一個 `LinkedList`:
```
?class?Stack?{
?public:
???_//?..._
?private:
???LinkedList?list_;
?};
```
Stack是否應該有一個返回`LinkedList`的`get()`方法?或者一個帶有`LinkedList`的`set()`方法?或者一個帶有`LinkedList`的構造函數?顯然,答案是“不”,因為應該從外向里設計接口。也就是說,`Stack`對象的用戶并不關心 `LinkedList`;他們只關心 pushing 和 popping。
現在看另一個更微妙的例子。假設 `LinkedList`類使用`Node`對象的鏈表來創建,每一個`Node`對象有一個指向下一個`Node`的指針:
```
?class?Node?{?/*...*/?};
?class?LinkedList?{
?public:
???//?...
?private:
???Node*?first_;
?};
```
`LinkedList`類是否應該有一個讓用戶訪問第一個`Node`的`get()`方法?`Node`對象是否應該有一個讓用戶訪問鏈中下一個 `Node`的 `get()`方法?換句話說,從外部看,`LinkedList`應該是什么樣的?`LinkedList` 是否實際上就是一個 `Node` 對象的鏈?或者這些只是實現的細節?如果只是實現的細節,`LinkedList` 將如何讓用戶在某時刻訪問 `LinkedList` 中的每一個元素?
某人的回答:`LinkedList` 不是的 `Node` 鏈。它可能的確是用? `Node` 創建的,但這不是本質。它的本質是元素的序列。因此,`LinkedList` 抽象應該提供一個“LinkedListIterator”,并且“LinkedListIterator”應該有一個`operator++` 來訪問下一個元素,并且有一對`get()`/`set()`來訪問存儲于`Node` 的值(`Node` 元素中的值只由`LinkedList`用戶負責,因此有一對`get()`/`set()`以允許用戶自由地維護該值)。
從用戶的觀點出發,我們可能希望 `LinkedList`類支持看上去類似使用指針算法訪問數組的 運算符:
```
?void?userCode(LinkedList&?a)
?{
???for?(LinkedListIterator?p?=?a.begin();?p?!=?a.end();?++p)
?????std::cout?<<?*p?<<?'\n';
?}
```
實現這個接口,`LinkedList`需要一個 `begin()`方法和 `end()`方法。它們返回一個“LinkedListIterator”對象。該“LinkedListIterator”需要一個前進的方法,`++p` ;訪問當前元素的方法,`*p`;和一個比較運算符,`p?!=?a.end()`。
如下的代碼,關鍵在于 `LinkedList` 類沒有任何讓用戶訪問 `Node` 的方法。`Node` 作為實現技術被完全地隱藏了。 `LinkedList` 類內部可能用雙重鏈表取代,甚至是一個數組,區別僅僅在于一些諸如`prepend(elem)` 和 `append(elem)`方法的性能上。
```
?#include?<cassert>????//?Poor?man's?exception?handling
?class?LinkedListIterator;
?class?LinkedList;
?class?Node?{
???//?No?public?members;?this?is?a?"private?class"_
???friend?LinkedListIterator;???//?友員類
???friend?LinkedList;
???Node*?next_;
???int?elem_;
?};
?class?LinkedListIterator?{
?public:
???bool?operator==?(LinkedListIterator?i)?const;
???bool?operator!=?(LinkedListIterator?i)?const;
???void?operator++?();???//?Go?to?the?next?element
???int&?operator*??();???//?Access?the?current?element
?private:
???LinkedListIterator(Node*?p);
???Node*?p_;
???friend?LinkedList;??//?so?LinkedList?can?construct?a?LinkedListIterator
?};
?class?LinkedList?{
?public:
???void?append(int?elem);????//?Adds?elem?after?the?end_
???void?prepend(int?elem);???//?Adds?elem?before?the?beginning
//?...
???LinkedListIterator?begin();
???LinkedListIterator?end();
???//?...
?private:
???Node*?first_;
?};
```
這些是顯然可以內聯的方法(可能在同一個頭文件中):
```
?inline?bool?LinkedListIterator::operator==?(LinkedListIterator?i)?const
?{
???return?p_?==?i.p_;
?}
?inline?bool?LinkedListIterator::operator!=?(LinkedListIterator?i)?const
?{
???return?p_?!=?i.p_;
?}
?inline?void?LinkedListIterator::operator++()
?{
???assert(p_?!=?NULL);??//?or?if?(p_==NULL)?throw?...
???p_?=?p_->next_;
?}
?inline?int&?LinkedListIterator::operator*()
?{
???assert(p_?!=?NULL);??//?or?if?(p_==NULL)?throw?...
???return?p_->elem_;
?}
?inline?LinkedListIterator::LinkedListIterator(Node*?p)
???:?p_(p)
?{?}
?inline?LinkedListIterator?LinkedList::begin()
?{
???return?first_;
?}
?inline?LinkedListIterator?LinkedList::end()
?{
???return?NULL;
?}
```
結論:鏈表有兩種不同的數據。存儲于鏈表中的元素的值由鏈表的用戶負責(并且只有用戶負責,鏈表本身不阻止用戶將第三個元素變成第五個),而鏈表底層結構的數據(如 `next` 指針等)值由鏈表負責(并且只有鏈表負責,也就是說鏈表不讓用戶改變(甚至看到!)可變的`next` 指針)。
因此 `get()`/`set()` 方法只獲取和設置鏈表的元素,而不是鏈表的底層結構。由于鏈表隱藏了底層的指針等結構,因此它能夠作非常嚴格的承諾(例如,如果它是雙重鏈表,它可以保證每一個后向指針都被下一個 `Node` 的前向指針匹配)。
我們看了這個例子,類的一些數據的值由用戶負責(這種情況下需要有針對數據的`get()`/`set()`方法),但對于類所控制的數據則不必有`get()`/`set()`方法。
注意:這個例子的目的不是為了告訴你如何寫一個鏈表類。實際上不要自己做鏈表類,而應該使用編譯器所提供的“容器類”的一種。理論上來說,要使用標準容器類之一,如:`std::list<T>` 模板。
- C++ FAQ Lite
- [1] 復制許可
- [2] 在線站點分發本文檔
- [3] C++-FAQ-Book 與 C++-FAQ-Lite
- [6] 綜述
- [7] 類和對象
- [8] 引用
- [9] 內聯函數
- [10] 構造函數
- [11] 析構函數
- [12] 賦值算符
- [13] 運算符重載
- [14] 友元
- [15] 通過 &lt;iostream&gt; 和 &lt;cstdio&gt;輸入/輸出
- [16] 自由存儲(Freestore)管理
- [17] 異常和錯誤處理
- [18] const正確性
- [19] 繼承 — 基礎
- [20] 繼承 — 虛函數
- [21] 繼承 — 適當的繼承和可置換性
- [22] 繼承 — 抽象基類(ABCs)
- [23] 繼承 — 你所不知道的
- [24] 繼承 — 私有繼承和保護繼承
- [27] 編碼規范
- [28] 學習OO/C++
- [31] 引用與值的語義
- [32] 如何混合C和C++編程
- [33] 成員函數指針
- [35] 模板 ?
- [36] 序列化與反序列化
- [37] 類庫