1. 程式人生 > 其它 >C++ 運算子過載詳解

C++ 運算子過載詳解

1. 運算子過載簡介

所謂過載,就是賦予新的含義。函式過載(Function Overloading)可以讓一個函式名有多種功能,在不同情況下進行不同的操作。同樣運算子過載(Operator Overloading)可以讓同一個運算子可以有不同的功能。

  • 可以對 int、float、string 等不同型別資料進行操作
    << 既是位移運算子,又可以配合 cout 向控制檯輸出資料

也可以自定義運算子過載:

class Complex
{
public:
    Complex();
    Complex(double real, double imag);
    Complex operator+(const Complex &a) const;
    void display() const; 
private:
    double m_real;
    double m_imag;
};

// ...

// 實現運算子過載
Complex Complex::operator+(const Complex &A) const{
    Complex B;
    B.m_real = this->m_real + A.m_real;
    B.m_imag = this -> m_imag + A.m_imag;
    return B;
    // return Complex(this->m_real + A.m_real, this->m_imag + A.m_imag);
}

int main(){
    Complex c1(4.3, 5.8);
    Complex c2(2.7, 3.7);
    Complex c3;
    c3 = c1 + c2;   // 運算子過載
    c3.display();
    return 0;
}
運算結果
7 + 9.5i

運算子過載其實就是定義一個函式,在函式體內實現想要的功能,當用到該運算子時,編譯器會自動呼叫這個函式,它本質上是函式過載。

c3 = c1 + c2;
實際上通過呼叫成員函式 operator+(),會轉換為下面的形式:
c3 = c1.operator+(c2);

全域性範圍內過載運算子

運算子過載函式不僅可以作為類的成員函式,還可以作為全域性函式。

複數加法運算通過全域性範圍內過載 + 實現:

class Complex{
    // ...
    friend complex operator+(const complex &A, const complex &B);
};

// 全域性函式
Complex operator+(const Complex &A, const Complex &B)
{
    return Complex(A.m_real + B.m_real, A.m_imag + B.m_imag);   // 訪問了Complex 的 private 成員變數,需要宣告為友元函式
}

2. 運算子過載時要遵循的規則

  • 能夠過載的運算子

    + - * / % ^ & | ~ ! = < > += = = /= %= ^= &= |= << >> <<= >>= == != <= >= && || ++ -- , -> - > () [] new new[] delete delete[]

    長度運算子 sizeof、條件運算子: ?、成員選擇符. 和域解析運算子::不能被過載。

  • 過載不能改變運算子的優先順序和結合律

  • 過載不會改變運算子的用法,即運算元個數、位置都不會改變

  • 運算子過載函式不能有預設的引數,因為這改變了運算子運算元個數

  • 運算子過載函式既可作為類成員函式,也可為全域性函式,注意全域性函式如何要訪問類物件的私有成員,需要宣告為類的友元

  • 箭頭運算子->、下標運算子[]、函式呼叫運算子()、賦值運算子 =,只能以成員函式的形式過載。

3. 過載運算子實現形式的選擇

過載運算子可以通過成員函式和全域性函式(友元)來實現

轉換建構函式

Complex c1(25, 25);
Complex c2 = c1 + 15.6;
Complex c3 = 28.23 + c1;    // 要以全域性函式實現過載

這幾行程式碼都可以順利執行,說明 Complex 物件可以和 double 型別物件相加。其實,編譯器在檢測到 Complex 和 double(小數預設為 double 型別)相加時,會先嚐試將 double 轉換為 Complex,或者反過來將 Complex 轉換為 double(只有型別相同的資料才能進行 + 運算),如果都轉換失
敗,或者都轉換成功(產生了二義性),才報錯。實際上,上述兩行加法程式碼會轉換為:

Complex c2 = operator+(c1,Complex(15.6));
Complex c3 = operator+(Complex(28.23),c1);  

Complex(double real)在作為普通建構函式的同時,還能將 double 型別轉換為 Complex 型別,集合了“建構函式”和“型別轉換”的功能,所以被稱為「轉換建構函式」。換句話說,轉換建構函式用來將其它型別(可以是 bool、int、double等基本型別,也可以是陣列、指標、結構體、類等構造型別)轉換為當前類型別。

以全域性函式形式過載

以全域性函式的形式過載了 +、-、*、/、==、!=,這樣做是為了保證這些運算子能夠被對稱的處理。

如果將 operator+定義為成員函式,根據“+ 運算子具有左結合性”這條原則,Complex c2 = c1 + 15.6;會被轉換為下面的形式:

Complex c2 = c1.operator+(Complex(15.6));

但是對於 Complex c3 = 28.23 + c1,編譯器會嘗試轉換為不同形式:

Complex c3 = (28.23).operator+(c1);

顯然這是錯誤的。

以成員函式的形式過載

以成員函式的形式過載了 +=、-=、 *=、/=。

運算子過載的初衷是給類新增新的功能,方便類的運算,它作為類的成員函式是理所應當的, 是首選的。不過類的成員函式不能對稱處理資料,運算子的第一個運算物件不會出現型別轉換(要類的物件才能呼叫類的成員函式)。

C++ 規定,箭頭運算子->、下標運算子[ ]、函式呼叫運算子( )、賦值運算子=只能以成員函式的形式過載。

4. 過載 >> 和 <<(輸入輸出運算子)詳解

C++ 標準庫已對 左移運算子 << 和 >> 右移運算子進行了承載,如果我們定義新的型別需要輸入輸出運算子去處理,需要再進行過載。

過載運算子 >>

以全域性函式的形式過載>>,使它能夠讀入兩個 double 型別的資料,並分別賦值給複數的實部和虛部:

istream & operator>>(istream &in, Complex &A)   // 友元
{
    in >> A.m_real >> A.m_image;
    return in;
}

之所以返回 istream 類 物件的引用,是為了能夠連續讀取複數,讓程式碼書寫更加漂亮,例如:

Complex c1,c2;
cin >> c1 >> c2;

如果不返回引用,需要一個一個讀取:

cin >> c1;
cin >> c2;

實際上,上述 >> 運算子會被轉換成如下形式:

operator<<(cin, c);

過載運算子 <<

ostream & operator<<(ostream &out, complex &A){
    out << A.m_real <<" + "<< A.m_imag <<" i ";
}

5. 過載 [] 下標運算子

下標運算子 [] 必須以成員函式的形式進行過載。

int& Array::operator[](int i){
    return m_p[i];
}

const int & Array::operator[](int i) const{
    return m_p[i];
}

當 Array 是 const 物件時,如果沒有提供 const 版本的 operator[],會報錯。

6. 過載 ++ 和 -- 自增自減運算子

class Stopwatch{
    // ... 秒錶類
public:
    Stopwatch operator++();     // ++i,前置形式 
    Stopwatch operator++(int);  // i++,後置形式
    Stopwatch run();    // 執行
private:
    int m_min;  // 分鐘
    int m_sec;  // 秒鐘
};

Stopwatch Stopwatch::run(){
    ++m_sec; 
    if(m_sec == 60){
        m_min++;
        m_sec = 0;
    } 
    return *this;
}

Stopwatch Stopwatch::operator++(){
    return run();
}

Stopwatch Stopwatch::operator++(int n){
    Stopwatch s = *this;    // i++, 先返回物件,再物件自增,所以需要將物件儲存
    run();
    return s;
}

函式中引數 n 是沒有任何意義的,它的存在只是為了區分是前置形式還是後置形式。

7. 過載 new 和 delete 運算子

記憶體管理運算子 new、new[]、delete 和 delete[] 也可以進行過載,其過載形式既可以是類的成員函式,也可以是全域性函式。一般情況下,內建的記憶體管理運算子就夠用了,只有在需要自己管理記憶體時才會過載。

// 成員函式
void * className::operator new( size_t size ){ 
    //TODO:
}

void className::operator delete( void *ptr){ 
    //TODO:
}

// 全域性函式
void * operator new( size_t size ){ 
    //TODO:
}

void operator delete( void *ptr){ 
    //TODO:
}

在過載 new 或 new[] 時,無論是作為成員函式還是作為全域性函式,它的第一個引數必須是 size_t 型別。size_t 表示的是要分配空間的大小,對於 new[] 的過載函式而言,size_t 則表示所需要分配的所有空間的總和。

size_t 在標頭檔案 中被定義為 typedef unsigned int size_t;,也就是無符號整型。

當然,過載函式也可以有其他引數,但都必須有預設值,並且第一個引數的型別必須是 size_t。

如果類中沒有定義 new 和 delete 的過載函式,那麼會自動呼叫內建的 new 和 delete 運算子。

8. 過載()(強制型別轉換運算子)

型別強制轉換運算子是單目運算子,也可以被過載,但只能過載為成員函式,不能過載為全域性函式。經過適當 過載後,(型別名)物件這個對物件進行強制型別轉換的表示式就等價於物件.operator 型別名(),即變成對運算子函式的呼叫。

class Complex{
public:
    // ...
    operator double()
    {
        return real;
    }
private:
    double m_real;
    double m_imag;
};

int main()
{
    Complex c(1.2,3.4);
    cout << (double)c << endl;  // 1.2
    double n = 2 + c;   // 等價於 double n = 2 + c.operator double()
}

9. 運算子過載總結

注意事項:

  • 過載後運算子的含義應該符合原有用法習慣。例如過載 + 運算子,完成的功能就應該類似於做加法,在過載的+ 運算子中做減法是不合適的。此外,過載應儘量保留運算子原有的特性。
  • C++ 規定,運算子過載不改變運算子的優先順序。
  • 以下運算子不能被過載:.、.*、::、? :、sizeof。
  • 過載運算子()、[]、->、或者賦值運算子=時,只能將它們過載為成員函式,不能過載為全域性函式。
  • 運算子的實質是將運算子過載為一個函式,使用運算子的表示式就會被解釋為對過載函式的呼叫。

  • 運算子可以過載為全域性函式。此時函式的引數個數就是運算子的運算元個數,運算子的運算元就成為函式的實參。(友元)

  • 運算子也可以過載為成員函式。此時函式的引數個數就是運算子的運算元個數減一,運算子的運算元有一個成為函式作用的物件,其餘的成為函式的實參。

  • 必要時需要過載賦值運算子=,以避免兩個物件內部的指標指向同一片儲存空間。

  • <<和>>是在 iostream 中被過載,才成為所謂的“流插入運算子”和“流提取運算子”的。

  • 型別的名字可以作為強制型別轉換運算子,也可以被過載為類的成員函式。它能使得物件被自動轉換為某種型別。

  • 自增、自減運算子各有兩種過載方式,用於區別前置用法和後置用法。

  • 運算子過載不改變運算子的優先順序。過載運算子時,應該儘量保留運算子原本的特性。