C++運算符重載 ++,--,+,-,+=,-=,輸出輸入運算符

https://blog.csdn.net/wenqian1991/article/details/34147997

一、運算符的定義

運算符重載就是運算符的“一符多用”。重載運算符是具有特殊名稱的函數:保留字 operator 後接需定義的操作符符號。像任意其他函數一樣,重載操作符具有返回類型和形參表,每個操作符用於內置類型都有關聯的定義,當內置操作符和類型上的操作存在邏輯對應關係時,操作符重載最有用,最直觀,使用重載操作符並不是創造命名操作。

二、在哪種情況下使用哪種重載運算符的方式合適?

C++ 提供了兩種重載運算符的方式,在大多數情況下:

只有一個操作數的運算符(一目)使用類運算符重載方式爲好;
一般地說,如果運算符要修改操作數(類對象)的狀態(值),則應使用類運算符(成員形式)。(在計算中可能改變操作數的值得運算符被稱爲有副作用的運算符,諸如:=、+=、-=、*=、/=、%=、++、-- 等);
C++規定,運算符=、()、[ ]、-> 只能採用類運算符形式重載;
有兩個操作數的運算符(二目)使用友元運算符重載方式爲好;
友元運算符重載方式在操作數的數據類型自動轉換方面更爲寬容,尤其是第一個操作數希望能夠隱式類型轉換時,則應採用友元形式;
不允許重載的運算符有:&&、||、. 、:: 、 * 、?: 。
三、運算符重載具體討論(返回值和參數,這裏討論幾個常用的運算符)

默認地,重載運算符必須與內置操作符保持一致,也就是說重載後的運算符必須與本來內置操作符保持特性一致。函數最主要的兩個就是返回值和形參。

3.1、前綴++類運算符重載函數(前綴--類似)

自增(自減)操作符的前置式定義:累加(遞減)而後取出;後置式定義:取出而後累加(遞減)。

我們知道,在C語言裏整型變量是允許連續前綴++兩次的,也叫鏈式操作。這樣爲了保證重載運算符與內置操作符++類型一致,就要求前綴++類運算符重載函數也支持連續操作(鏈式操作),所以前綴++類運算符重載函數的返回值必須是類名的引用。上面第二點也說了,++作爲單目運算符,並且會修改操作數的值,則應定義爲類運算符,這樣重載函數無形參。我們就可申明該前綴++類運算符重載函數如下:

class Zoo
{
public:
    Zoo(int lion_n = 0, int tiger_n = 0){
        lion = lion_n; tiger = tiger_n; }
    ~Zoo(){}
 
    Zoo& operator++();//無參,返回值爲類名的引用
private:
    int lion;
    int tiger;
};
下面就是前綴++類運算符重載函數的實現了
內置類型前綴++操作符是直接修改了操作數,然後返回修改後的操作數本身(唯一地址),不存在複製的情況,所以重載函數也應遵循這一點:

Zoo& Zoo::operator++()
{
    ++lion;
    ++tiger;
    return *this;
}

由於函數的返回值類型被定義爲引用,所以不會發生複製,返回的是操作數本身,完全符合內置前綴++的語法定義。
我們再來考慮錯誤情況:如果前綴++類運算符重載函數的返回值是類型,也就是返回一個對象,其對應實現如下:

Zoo Zoo::operator++()
{
    ++lion;
    ++tiger;
    return *this;
}
咋一看上面的也實現了前綴++的功能,但是返回值是對象,在函數返回時會發生複製,雖然該函數成功將操作類對象的成員修改了,但是函數返回的是一個複製品,然後再執行++鏈式操作時,修改的會是這個複製品的值(相當於這個複製品調用前綴++類運算符重載函數),本尊並沒有修改,也就是不能成功實現鏈式操作,不符合內置++的語法定義(C++中,前綴++是可以連續前綴兩次以上的,但後綴++則不可以)。
Zoo zoo;
++(++zoo);
上面執行後,zoo.lion = 1,zoo.tiger = 1 。並不是期望的2。
至於返回其餘類型那就更加錯誤了。

3.2、後綴++類運算符重載函數(後綴--類似)

與前綴++操作符一樣,後綴++也是單目操作符,也會修改操作書本身,所以二者的形參數目和類型相同,爲了區別函數,後綴++操作符接受一個額外的(即,無用的)int 型形參。使用後綴++操作符時,編譯器提供0作爲這個形參的實參。

與前綴++類運算符截然相反的是,後綴++返回值的類型恰恰不能是類的引用,其目的是在返回值時引起復制,即讓一個並未自增的替身對象去參加表達式的後續運算,另外C/C++在語法上不允許後綴++連續運算兩次以上,也就不要求返回引用,並且必須返回 const 對象。我們看看內置後綴++操作符:

int i = 0;
int j = i++;
i++++;  //違法
內置後綴++操作符,操作數 i 本身已經完成了自增,但是後續的賦值操作並不是將自增後的 i 賦值給j,而是將並未自增的替身參與賦值運算。所以在重載後綴++類運算符的時候,我們應該考慮這點,另外必須返回一個 const 對象:
const Zoo Zoo::operator++(int)
{
    Zoo ret(*this);//拷貝構造函數,構造複製品
    ++lion;//本尊自增
    ++tiger;
    return ret;//返回複製品
}
在已經定義了前綴++類運算符重載函數的情況下,後綴++類運算符重載函數一般這樣實現:
const Zoo Zoo::operator++(int)
{
    Zoo ret(*this);//拷貝構造函數,構造複製品
    ++(*this);//本尊自增
    return ret;//返回複製品
}
3.3、二目運算符重載(+=,-=,+,-)
先說複合賦值操作符,上面“+=”,“-=”也可認爲是賦值操作符。內置+=、-=、%= 是允許進行鏈式操作的(如果不確定是否允許,可以寫一個測試程序判斷),所以爲了與內置類型的操作一致,重載函數毫無疑問是返回一個引用,也避免了創建和撤銷結果的臨時副本。

但是“+”“-” 等是返回一個新的結果,這就要求算術運算符的重載不能返回一個引用,另外+的表達式也不能作爲左值。

    int i = 1;
    int j = 2;
    i = i + j + j;//可以連續+,但是右邊的i,j還是原值,(i+j) = i + i;錯誤!
        i += (i += j);//複合了賦值操作符,這樣是允許的
有了前面分析,不難寫出上面的重載函數
Zoo& Zoo::operator+=(Zoo &rhs)
{
    lion += rhs.lion;
    tiger += rhs.tiger;
    return *this;
}
繼而過來討論“+”“-”:
返回值是一個右值

前面說到了,“+”“-”是返回一個新的結果,算術運算符通常產生一個新值,該值是兩個操作數的計算結果,它不同於任一操作數且在一個局部變量中計算,返回對那個變量的引用是一個運行時錯誤。通俗一點,假如算術運算符重載函數返回一個對象的引用,這個引用是兩個操作的計算結果,它的本體就會是一個局部變量(對象),返回一個局部變量的引用,是錯誤的。所以對於類算術運算符的重載,只能返回一個右值。

Zoo operator+(Zoo &first, Zoo &second)
{
    Zoo ret(first);//拷貝構造函數,構造一個局部變量,用於返回值
    ret += second;//運算操作
 
    return ret;//返回一個值
}
二目算術運算符重載通常使用友元運算符重載方式。
從上面也可以看出,類運算符的重載最好與內置運算符保持一致,雖然沒硬性規定,但這儼然成了一個默認規定。

另外 !(邏輯反)、~(按位與)、-(負號)等與二目算術運算符有類似之處,那就他們都不會修改原對象數據成員,而是將運算結果交給一個新值,所以在重載時,需要構造一個臨時對象作爲返回值,返回值也就同樣不能是引用。

3.4、輸入輸出操作符重載

支持I/O操作的類所提供的I/O操作接口,一般應該與標準庫iostream爲內置類型定義的接口相同。

1、輸出操作符 <<  的重載

爲了與I/O標準庫一致,操作符應接受 ostream& 作爲第一個形參,對類類型 const 對象的引用作爲第二個形參,並返回對 ostream 形參的引用。

重載輸出操作符可能相對於比較難理解,這裏簡單的說下,我們只能以自定義類的友元函數的形式重載這兩個運算符,這是因爲如果我們用成員函數的形式來重載的話,就要改動系統的流類 istream 和 ostream 定義,這是C++不允許的,如果不定義爲友元函數的話,將無法調用類對象成員數據輸出。

ostream& operator<<(ostream& stream, const Zoo &object)
{
    //對object所引用的對象的數據進行的輸出操作
    stream << object.lion;
    stream << object.tiger;
 
    return stream;
}
我們來看看上面這個輸出操作符重載函數,第一個參數是 ostream 類的引用,而函數的返回值也是 ostream 類的引用。毫無疑問,我們調用這個運算符重載函數時。實參肯定是 cout,這樣就造成了這樣一種情況:實參 stream 引用 cout,而函數的返回值又引用 stream,等於函數返回值引用的實體還是 cout。這樣做的目的是實現了連續的輸出操作。當執行下面語句:

    cout << zoo_a << zoo_b;
    //上面 cout << zoo_a 實質就是調用 operator<<(cout, zoo_a),然後返回 cout
    //下一個 << 就相當於執行 cout << zoo_b, 同上
我們不能將該操作符重載函數定義爲類的成員函數,否則,左操作數將只能是該類類型的對象。IO操作符通常要對非公用數據成員進行讀寫,因此,類通常將IO操作符(輸入輸出)設爲友元。
2、輸入操作符 >> 的重載

爲了與IO標準庫一致,操作符應接受 istream& 作爲第一個形參,指向它要讀的流,並且返回的也是對同一個流的引用(鏈式操作)。它的第二個形參是對要讀入的對象的非 const 引用,該形參必須爲非 const,因爲輸入操作符的目的是將數據讀到這個對象中。

更重要但通常重視不夠的是,輸入和輸出操作符有如下區別:輸入操作符必須處理錯誤和文件結束的可能性。

輸入期間的錯誤:任何讀操作都可能因爲提供的值不正確而失敗;任何讀入都可能碰到輸入流中的文件結束或其他一些錯誤。也就需要對輸入進行附加檢查,發現有這些錯誤就需要我們進行處理。

3.5、不能重載的運算符 &&、|| 和 , 操作符

和 C 一樣,C++ 對於真假值表達式採用所謂驟死式評估方式。意思是一旦該表達式的真假值去頂,縱使表達式中還有部分尚未檢驗,整個評估工作仍告結束。比如下面這種情況:

char *p;
……
if ((p != NULL) && (strlen(p) > 10) ……
你無需擔心調用 strlen 時 p 是否爲 NULL 指針,因爲如果 p 是否爲NULL 的測試結果是否定的,strlen 就絕不會被調用。事實上,對一個 NULL 指針調用 strlen,結果未可預期。
回到重載,C++ 允許我們爲用戶定製型別量身定做各類操作符,包括 && 和 ||,操作符重載語義上是允許的,但是我們要考慮重載會不會改變對應內置操作符的規則。拿 && 和 || 來說,重載則是對 operator && 和 operator || 兩函數進行重載工作,值得注意的是,函數調用語義將會取代驟死式語義,也就是說,如果你將operator && 重載,下面這個蝨子:

if (expression1 && expression2) ……
會被編譯器視爲以下兩者之一:
if (expression1.operator&&(expression2)) ……
//假設 operator&& 是個 member function
if (operator&&(expression1, expression2)) ……
//假設 operator&& 是個全局函數
上面函數調用語義和所謂驟死式語義有兩個重大的不同。第一,當函數調用動作被執行起來,所有參數值都必須評估完成,所以當我們調用 operator&& 和 operator|| 時,兩個參數都已評估完成,沒有什麼驟死式語義。第二,C++ 語言規格並未明定函數調用動作中各參數的評估次序,所以沒辦法知道 expression1 和 expression2 哪個會先被評估,而內置的真假值表達式,則總是由左向右評估其自變量。
C++ 中,運算符重載的一個重要參考就是:不能修改運算符的內置語義。
————————————————
版權聲明:本文爲CSDN博主「selfimpr1991」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/wenqian1991/article/details/34147997

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章