一、More Effective C++:不要重載的操作符
與C一樣,C++使用布林運算式簡化求值法(short-circuit evaluation)。這表示一旦確定了布林運算式的真假值,即使還有部分運算式沒有被測試,布林運算式也停止運算。例如:
- char *p;
- ...
- if ((p != 0) && (strlen(p) > 10)) ...
這裡不用擔心當p為空白時strlen無法正確運行,因為如果p不等於0的測試失敗,strlen不會被調用。同樣:
- int rangeCheck(int index)
- {
- if ((index < lowerBound) || (index > upperBound)) ...
- ...
- }
如果index小於lowerBound,它不會與upperBound進行比較。
很早以前上述行為特性就被反覆灌輸給C和C++的程式員,所以他們都知道該特性。而且他們也依賴於簡短求值法來寫程式。例如在上述第一個代碼中,當p為空白指標時確保strlen不會被調用是很重要的,因為C++標準說(正如C標準所說)用null 指標調用strlen,結果不確定。
C++允許根據使用者定義的類型,來定製&&和||操作符。方法是重載函數operator&& 和operator||,你能在全域重載或每個類裡重載。然而如果你想使用這種方法,你必須知道你正在極大地改變遊戲規則。因為你以函數調用法替代了簡短計演算法。也就是說如果你重載了操作符&&,對於你來說代碼是這樣的:
- if (expression1 && expression2) ...
對於編譯器來說,等同於下面代碼之一:
- if (expression1.operator&&(expression2)) ...
- // when operator&& is a
- // member function
- if (operator&&(expression1, expression2)) ...
- // when operator&& is a
- // global function
這好像沒有什麼不同,但是函數調用法與簡短求值法是絕對不同的。首先當函數被調用時,需要運算其所有參數,所以調用函數functions operator&& 和 operator||時,兩個參數都需要計算,換言之,沒有採用簡短計演算法。第二是C++語言規範沒有定義函數參數的計算順序,所以沒有辦法知道運算式1與運算式2哪一個先計算。完全與具有從左參數到右參數計算順序的簡短計演算法相反。
因此如果你重載&&或||,就沒有辦法提供給程式員他們所期望和使用的行為特性,所以不要重載&&和||。
同樣的理由也適用於括弧操作符,但是在我們深入研究它之前,我還是暫停一下,讓你不要太驚訝,“逗號操作符?哪有逗號操作符?”確實存在。
逗號操作符用於組成運算式,你經常在for迴圈的更新部分(update part)裡遇見它。例如下面來源於Kernighan's and Ritchie's 經典書籍The C Programming Language 第二版(Prentice-Hall, 1988)的函數:
- // reverse string s in place
- void reverse(char s[])
- {
- for (int i = 0, j = strlen(s)-1;i < j;++i, --j) // 啊! 逗號操作符!
- {
- int c = s[i];
- s[i] = s[j];
- s[j] = c;
- }
- }
在for迴圈的最後一個部分裡,i被增加同時j被減少。在這裡使用逗號很方便,因為在最後一個部分裡只能使用一個運算式,分開運算式來改變i和j的值是不合法的。
對於內建類型&&和||,C++有一些規則來定義它們如何運算。與此相同,也有規則來定義逗號操作符的計算方法。一個包含逗號的運算式首先計算逗號左邊的運算式,然後計算逗號右邊的運算式;整個運算式的結果是逗號右邊運算式的值。所以在上述迴圈的最後部分裡,編譯器首先計算++i,然後是—j,逗號運算式的結果是--j。
也許你想為什麼你需要知道這些內容呢?因為你需要模仿這個行為特性,如果你想大膽地寫自己的逗號操作符函數。不幸的是你無法模仿。
如果你寫一個非成員函數operator,你不能保證左邊的運算式先於右邊的運算式計算,因為函數(operator)調用時兩個運算式做為參數被傳遞出去。但是你不能控制函數參數的計算順序。所以非成員函數的方法絕對不行。
剩下的只有寫成員函數operator的可能性了。即使這裡你也不能依靠於逗號左邊運算式先被計算的行為特性,因為編譯器不一定必須按此方法去計算。因此你不能重載逗號操作符,保證它的行為特性與其被料想的一樣。重載它是完全輕率的行為。
你可能正在想這個重載惡夢究竟有沒有完。畢竟如果你能重載逗號操作符,你還有什麼不能重載的呢?正如顯示的,存在一些限制,你不能重載下面的操作符:
- . .* :: ?:
- new delete sizeof typeid
- static_cast dynamic_cast const_cast reinterpret_cast
你能重載:
- operator new operator delete
- operator new[] operator delete[]
- + - * / % ^ & | ~
- ! = < > += -= *= /= %=
- ^= &= |= << >> >>= <<= == !=
- <= >= && || ++ -- , ->* ->
- () []
當然能重載這些操作符不是去重載的理由。操作符重載的目的是使程式更容易閱讀,書寫和理解,而不是用你的知識去迷惑其他人。如果你沒有一個好理由重載操作符,就不要重載。在遇到&&, ||, 和 ,時,找到一個好理由是困難的,因為無論你怎麼努力,也不能讓它們的行為特性與所期望的一樣。
二、重載操作符的設計
①類的設計者不能聲明一個沒有預定義的重載操作符。
②不能為內建資料類型定義其他的操作符。
③預定義的操作符優先順序不能被改變。
④一個類最終需要提供哪些操作符,是由該類預期的用途來決定的。
三、prefix and postfix
為區分後置操作符與前置操作符的聲明,重載的遞增和遞減後置操作符的聲明有一個額外的int 類型的參數。這裡不需要給出參數名,因為它沒有被用在操作符定義中。額外的整型參數對於後置操作符的使用者是透明的,編譯器為它提供了預設值因而該參數也可以被忽略。
- 例如:
- #include
- #include
- using namespace std;
- class person
- {
- private:
- int age;
- public:
- person(int a)
- {
- age=a;
- }
- person const operator++()/*prefix ++ */
- {
- this->age++;
- return *this;
- }
- person const operator++(int a)/*postfix ++ */
- {
- person temp(1);
- temp=*this;
- this->age++;
- return temp;
- }
- int GetAge()
- {
- return age;
- }
- };
- int main()
- {
- person rose(10);
- person jack(20);
- person marry(22);
- person tom(30);
- jack=++rose;
- marry= tom++;
- cout<cout<return 0;
- }
四、重載的建議
當一個重載操作符是一個名字空間的函數時,對於操作符的第一個和第二個參數,即等於操作符的左和右兩個運算元,都會考慮轉換.
一般應該怎樣決定是把一個操作符聲明為類成員還是名字空間成員呢?在某些情況下程式員沒有選擇的餘地:
1 如果一個重載操作符是類成員,那麼只有當跟它一起被使用的左運算元是該類的對象時,它才會被調用.如果該操作符的左運算元必須是其他的類型,那麼重載操作符必須是名字空間成員.
2 C++要求賦值= 下標[] 調用() 和成員訪問箭頭-> 操作符必須被定義為類成員操作符.任何把這些操作符定義為名字空間成員的定義都會被標記為編譯時間刻錯誤.
例如:
// 錯誤: 必須是類成員
char& operator[]( String & ,int ix );
3 除此之外由類設計者選擇把操作符聲明為一個類成員還是一個名字空間成員.如果有一個運算元是類類型,如String 類的情形,那麼對於對稱操作符,比如等於操作符最好定義為名字空間成員.