當運算符作用于類類型的運算對象時,可以通過運算符重載重新定義該運算符的含義。明智地使用運算符重載能令我們的程序更易于編寫和閱讀。舉個例子,因為在Sales_item類中定義了輸入、輸出和加法運算符,所以可以通過下述形式輸出兩個Sales_item的和:
cout << item1 + item2;//輸出兩個Sales item的和
相反的,由于我們的sales_data類還沒有重載這些運算符,因此它的加法代碼顯得比較冗長而不清晰:
print(cout,add(data1,data2));//輸出兩個Sales data的和
14.1 基本概念
重載的運算符是具有特殊名字的函數:它們的名字由關鍵字operator和其后要定義的運算符號共同組成。和其他函數一樣,重載的運算符也包含返回類型、參數列表以及函數體。
重載運算符函數的參數數量與該運算符作用的運算對象數量一樣多。一元運算符有一個參數,二元運算符有兩個。對于二元運算符來說,左側運算對象傳遞給第一個參數,而右側運算對象傳遞給第二個參數。除了重載的函數調用運算符operator()之外,其他重載運算符不能含有默認實參。
如果一個運算符函數是成員函數,則它的第一個(左側)運算對象綁定到隱式的this指針上,因此,成員運算符函數的(顯式)參數數量比運算符的運算對象總數少一個。
Note:當一個重載的運算符是成員函數時,this綁定到左側運算對象。成員運算符函數的(顯式)參數數量比運算對象的數量少一個。
對于一個運算符函數來說,它或者是類的成員,或者至少含有一個類類型的參數:
//錯誤:不能為int 重定義內置的運算符
int operator+(int,int);
這一約定意味著當運算符作用于內置類型的運算對象時,我們無法改變該運算符的含義。
我們可以重載大多數(但不是全部)運算符。表14.1指明了哪些運算符可以被重載,哪些不行。我們將在19.1.1節(第726頁)介紹重載new和delete的方法。
我們只能重載已有的運算符,而無權發明新的運算符號。例如,我們不能提供operator**來執行冪操作。
有四個符號(+、-、*、&)既是一元運算符也是二元運算符,所有這些運算符都能被重載,從參數的數量我們可以推斷到底定義的是哪種運算符。
對于一個重載的運算符來說,其優先級和結合律與對應的內置運算符保持一致。不考慮運算對象類型的話,
X == y + Z; 永遠等價于x==(y+z)。
直接調用一個重載的運算符函數
通常情況下,我們將運算符作用于類型正確的實參,從而以這種間接方式“調用”重載的運算符函數。然而,我們也能像調用普通函數一樣直接調用運算符函數,先指定函數名字,然后傳入數量正確、類型適當的實參:
//一個非成員運算符函數的等價調用
data1 + data2;//普通的表達式
operator+(datal,data2);//等價的函數調用
這兩次調用是等價的,它們都調用了非成員函數operator+,傳入 data1作為第一個實參、傳入 data2作為第二個實參。
我們像調用其他成員函數一樣顯式地調用成員運算符函數。具體做法是,首先指定運行函數的對象(或指針)的名字,然后使用點運算符(或箭頭運算符)訪問希望調用的函數:
data1+= data2;//基于“調用”的表達式
data1.operator+=(data2);//對成員運算符函數的等價調用
這兩條語句都調用了成員函數 operator+=,將this 綁定到 data1的地址、將 data2作為實參傳入了函數。
某些運算符不應該被重載
回憶之前介紹過的,某些運算符指定了運算對象求值的順序。因為使用重載的運算符本質上是一次函數調用,所以這些關于運算對象求值順序的規則無法應用到重載的運算符上。特別是,邏輯與運算符、邏輯或運算符和逗號運算符的運算對象求值順序規則無法保留下來。除此之外,&&和||運算符的重載版本也無法保留內置運算符的短路求值屬性,兩個運算對象總是會被求值。
因為上述運算符的重載版本無法保留求值順序和/或短路求值屬性,因此不建議重載它們。當代碼使用了這些運算符的重載版本時,用戶可能會突然發現他們一直習慣的求值規則不再適用了。
還有一個原因使得我們一般不重載逗號運算符和取地址運算符:C++語言已經定義了這兩種運算符用于類類型對象時的特殊含義,這一點與大多數運算符都不相同。因為這兩種運算符已經有了內置的含義,所以一般來說它們不應該被重載,否則它們的行為將異于常態,從而導致類的用戶無法適應。
BestPrntices 通常情況下,不應該重載逗號、取地址、邏輯與和邏輯或運算符。
使用與內置類型一致的含義
當你開始設計一個類時,首先應該考慮的是這個類將提供哪些操作。在確定類需要哪些操作之后,才能思考到底應該把每個類操作設成普通函數還是重載的運算符。如果某些操作在邏輯上與運算符相關,則它們適合于定義成重載的運算符:
(1)如果類執行I0操作,則定義移位運算符使其與內置類型的I0保持一致。
(2)如果類的某個操作是檢查相等性,則定義operator==;如果類有了operator==,意味著它通常也應該有operator!=。
(3)如果類包含一個內在的單序比較操作,則定義operator<;如果類有了operator<,則它也應該含有其他關系操作。
(4)重載運算符的返回類型通常情況下應該與其內置版本的返回類型兼容:邏輯運算符和關系運算符應該返回 bool,算術運算符應該返回一個類類型的值,賦值運算符和復合賦值運算符則應該返回左側運算對象的一個引用。
提示:盡量明智地使用運算符重載
每個運算符在用于內置類型時都有比較明確的含義。以二元+運算符為例,它明顯執行的是加法操作。因此,把二元+運算符映射到類類型的一個類似操作上可以極大地簡化記憶。例如對于標準庫類型string來說,我們就會使用+把一個string對象連接到另一個后面,很多編程語言都有類似的用法。
當在內置的運算符和我們自己的操作之間存在邏輯映射關系時,運算符重載的效果最好。此時,使用重載的運算符顯然比另起一個名字更自然也更直觀。不過,過分濫用運算符重載也會使我們的類變得難以理解。
在實際編程過程中,一般沒有特別明顯的濫用運算符重載的情況。例如,一般來說沒有哪個程序員會定義operator+并讓它執行減法操作。然而經常發生的一種情況是,程序員可能會強行扭曲了運算符的“常規”含義使得其適應某種給定的類型,這顯然是我們不希望發生的。因此我們的建議是:只有當操作的含義對于用戶來說清晰明了時才使用運算符。如果用戶對運算符可能有幾種不同的理解,則使用這樣的運算符將產生二義性。
賦值和復合賦值運算符
賦值運算符的行為與復合版本的類似:賦值之后,左側運算對象和右側運算對象的值相等,并且運算符應該返回它左側運算對象的一個引用。重載的賦值運算應該繼承而非違背其內置版本的含義。
如果類含有算術運算符或者位運算符,則最好也提供對應的復合賦值運算符。無須言,+=運算符的行為顯然應該與其內置版本一致,即先執行+,再執行=。
選擇作為成員或者非成員
當我們定義重載的運算符時,必須首先決定是將其聲明為類的成員函數還是聲明為一個普通的非成員函數。在某些時候我們別無選擇,因為有的運算符必須作為成員:另一些情況下,運算符作為普通函數比作為成員更好
下面的準則有助于我們在將運算符定義為成員函數還是普通的非成員函數做出抉擇:
(1)賦值(=)、下標([])、調用(())和成員訪問箭頭(->)運算符必須是成員。
(2)復合賦值運算符一般來說應該是成員,但并非必須,這一點與賦值運算符略有不同。
(3)改變對象狀態的運算符或者與給定類型密切相關的運算符,如遞增、遞減和解引用運算符,通常應該是成員。
(4)具有對稱性的運算符可能轉換任意一端的運算對象,例如算術、相等性、關系和位運算符等,因此它們通常應該是普通的非成員函數。
程序員希望能在含有混合類型的表達式中使用對稱性運算符。例如,我們能求一個int和一個double的和,因為它們中的任意一個都可以是左側運算對象或右側運算對象,所以加法是對稱的。如果我們想提供含有類對象的混合類型表達式,則運算符必須定義成非成員函數。
當我們把運算符定義成成員函數時,它的左側運算對象必須是運算符所屬類的一個對象。例如:
string s="world";
string t=s+"!"; //正確:我們能把一個const char*加到一個string 對象中
string u="hi"+s;//如果+是string的成員,則產生錯誤
如果 operator+是 string 類的成員,則上面的第一個加法等價于s.operator+(“!”)。同樣的,“hi”+s等價于"hi".operator+(s)。顯然"hi"的類型是const char*,這是一種內置類型,根本就沒有成員函數。
因為string將+定義成了普通的非成員函數,所以"hi"+s等價于operator+(“hi”,s)。和任何其他函數調用一樣,每個實參都能被轉換成形參類型。唯一的要求是至少有一個運算對象是類類型,并且兩個運算對象都能準確無誤地轉換成string。