重載運算

重載的運算符是具有特殊名字的函數:它們的名字由關鍵字 operator 和其後要定義的運算符號共同組成。和其他函數一樣,重載的運算符也包含返回類型、參數列表以及函數體。對於一個運算符函數來說,它或者是類的成員,或者至少含一個類類型的參數:

//錯誤:不能爲int重定義內置的運算符
int operator+(int, int);

重載運算符函數的參數與該運算符作用的運算對象數量一樣多,一元運算符有一個參數,二元運算符有兩個。

...
//爲了訪問ope對象的私有成員,將該運算符函數聲明爲ope的友元函數
ope& operator+ (ope& lhs, ope& rhs)
{
    lhs.x += rhs.x;
    return lhs;
}

當一個重載的運算符是成員函數時,this綁定到左側運算對象。成員運算符函數的(顯示)參數數量比運算對象的數量少一個。例如上面的 +、+=這兩個運算符本來是二元運算符應該有兩個顯示的參數,但是由於重載運算符函數是成員函數,所以this綁定到左側運算對象,只有一個參數傳入右側對象。另外,輸入輸出重載運算符必須是普通的非成員函數。
下面是一些成員函數重載運算符以及輸出重載運算符的例子:

#include<iostream>
using namespace std;
class ope
{
public:

    friend ostream& operator<< (ostream&, ope&);
    friend ope& operator+ (ope& lhs, ope& rhs);

    ope(){}
    ope(int i):x(i){}

    ope& operator+= (ope& rhs){
        x += rhs.x;
        return (*this);
    }

    ope& operator+(ope& rhs)
    {
        return (*this += rhs);
    }

    int getvalue() const{
        return x;
    }
private:
    int x;
};
//重載輸出運算符
ostream& operator<< (ostream& out, ope& rhs)
{
    out << rhs.x;
    return out;
}

測試代碼爲:

#include<iostream>
#include"operator.h"
int main(int argc,char *argv[])
{
    ope A(2);
    ope B(3);

    std::cout << A+B << std::endl;
    return 0;
}

通常情況下,我們會像內置類型那樣使用運算符。我們也可以像調用普通函數一樣直接調用運算符函數,先指定函數名字,然後再傳入數量正確、類型適當的實參:

...
std::cout << A.operator+=(B) << std::endl;
std::cout << operator+(A, B) << std::endl;
...

通常情況下,不應該重載逗號、取地址、邏輯與和邏輯或運算符。因爲邏輯與和邏輯或沒辦法保留短路求值屬性,而逗號和取地址符在C++裏已經有了內置含義,爲了避免它們的行爲異於常態,所以儘量不要重載它們。

重載運算符的返回類型通常情況下應該與其內置版本的返回類型兼容:邏輯運算符關係運算符都應該返回bool,算數運算符應該返回一個類類型的值,賦值運算符和複合賦值運算符則應該返回左側運算對象的一個引用。

當我們在定義重載的運算符時,必須首先決定是將其聲明爲類的成員函數還是聲明爲一個普通的非成員函數。有一些選擇的準則如下:

賦值(=)、下標([])、調用(())和成員訪問箭頭(->)運算符必須是成員。
複合賦值運算符一般來說應該是成員,但並非必須,這一點與賦值運算符略有不同。
改變對象狀態的運算符或者與給定類型密切相關的運算符,如遞增、遞減和解引用運算符,通常應該是成員。
具有對稱性的運算符可能任意轉換一端的運算對象,例如,相等性,關係和位運算等通常應該是普通的非成員函數
加法是對稱的,因爲它的兩個對象都可能在左側也可能在右側。
如果想提供含有類對象的混合類型表達式,則運算符必須定義成非成員函數

當我們把運算符定義成成員函數時,它的左側對象必須是運算符所屬類的一個對象!

輸入輸出運算符必須是非成員函數,因爲輸入輸出流對象必須是左側運算對象,而當運算符是成員函數時,this綁定到左側運算對象上,左側對象必須是運算符所屬類的對象。即,輸入輸出流對象必須是該自定義類的對象,這和輸入輸出流對象不是自定義類對象(是標準庫中類的對象)相互矛盾。

在上述中,爲了介紹重載運算符的概念,貼了一個代碼,但是有些地方並不符合代碼規範,或者是不符合我們平時的代碼編寫習慣,下面我們就一些常見的運算符加以介紹。聲明爲常量證明運算符一般不需要改變對象的狀態,聲明爲引用是爲了減少複製。

算術運算符定義成非成員函數(通常),計算兩個對象然後存在一個局部變量內,返回局部變量的拷貝作爲結果。

ope operator+ (const ope& lhs, const ope& rhs)
{
    ope sum = lhs;
    sum += rhs;
    return sum;
}

如果某個類在邏輯上有相等性的含義,則該類應該定義operator==,這樣做可以使得用戶更容易使用標準庫算法來處理這個類,定義函數實現相同的功能會有記憶函數名和可能錯誤使用未定義運算符的困擾。一般定義了operator==也會定義operator!= 運算符:

...
private:
    int x_;
    std::string name_;
};

bool operator==(const ope& lhs, const ope& rhs)
{
    return (lhs.x_ == rhs.x_ && lhs.name_ == rhs.name_);
}
bool operator!=(const ope& lhs, const ope& rhs)
{
    return !(lhs == rhs);
}

對於關係運算符來說,比如operator<,並不是只要有定義了相等運算符就需要關係運算符的。假設我們同時定義了相等和小於運算符,那麼當兩個對象不相等時肯定會有一個對象小於另外一個對象:

A.name_ != B.name_;

如上述代碼,兩個對象名不相等,但是我們完全沒有必要比較哪個對象名大。因此,對於自定義類來說,如果不存在邏輯可靠有必要的<定義,那這個類不定義<運算符也許會更好。

下標運算符必須是成員函數

using namespace std;
class StrVec
{
public:
    StrVec(){}
    StrVec(string& s):s_(s){}

    char& operator[](std::size_t n){
        return s_[n];
    }
    const char& operator[](std::size_t n) const{
        return s_[n];
    }
    string& getdata(){
        return s_;
    }
    const string& getdata() const{
        return s_;
    }
private:
    string s_;
};
#include<iostream>
#include"operator2.h"
int main(int argc,char *argv[])
{
     string s = "hello";
    StrVec A(s);
    const StrVec B(s);

    A[0] = 'k';
    std::cout << A.getdata() << std::endl;

    //B[0] = 'k'; 會報錯
    std::cout <<B.getdata() << std::endl;

    return 0;
}

爲了與下標的原始定義兼容,下標運算符通常以所訪問元素的引用作爲返回值,這樣做的好處是下標可以出現在賦值運算符的任意一端。進一步,如果一個類包含下標運算符,則它通常會定義返回普通引用的普通成員版本和返回常量引用的常量成員版本 常量版本確保在給 常量對象 取下標時不會改變對象的內容(即不能賦值)

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