C++運算符重載詳解

C++語言的一個頗有意思的特性就是除了支持函數重載外還支持運算符重載,緣由就是在C++看來運算符也算是一種函數。好比一個 a + b 的加法表達式也能夠用函數的形式:operator + (a, b)來表達。這裏的operator +表明的就是加法函數。高級語言中的表達式和數學表達式很是類似,在必定的程度上經過運算符來描述表達式會比經過函數來描述表達式更加利於理解和閱讀。通常狀況下在重載某個運算符的實現時最好要和運算符自己的數學表示意義類似,固然你也能夠徹底實現一個和運算符自己意義無關的功能或者相反的功能(好比對某個+運算符實現爲相減)。運算符函數和類的成員函數以及普通函數同樣,一樣可分爲類運算符和普通運算符。要定義一個運算符函數老是按以下的格式來定義和申明:ios

返回類型 operator 運算符(參數類型1 [,參數類型2] [,參數類型3] [, 參數類型N]);
複製代碼

運算符重載須要在運算符前面加上關鍵字operator。通常狀況下參數的個數不會超過2個,由於運算符大多隻是一元或者二元運算,而只有函數運算符()以及new和delete這三個運算符才支持超過2個參數的狀況。c++

可重載的運算符的種類

並非全部C++中的運算符均可以支持重載,咱們也不能建立一個新的運算符出來(好比Σ)。有的運算符只能做爲類成員函數被重載,而有的運算符則只能當作普通函數來使用。算法

  • 不能被重載的運算符有:. .* :: ?: sizeof
  • 只能做爲類成員函數重載的運算符有:() [] -> =

下面我將會對各類運算符重載的方法進行詳細的介紹。同時爲了更加表現通用性,我這邊對參數類型的定義都採用模板的形式,並給出運算符的一些大致實現的邏輯。實際中進行重載時則須要根據具體的類型來進行定義和聲明編程

1. 流運算符
描述
運算符種類 >> <<
是否支持類成員 YES
是否支持普通函數 YES
運算單元 二元
返回類型 左值引用

流運算符是C++特有的一種運算符。C++的標準庫裏面的iostream類就支持了流運算符並提供了讀取流>>和插入流<<兩種運算符,它們分別用來進行輸入和輸出操做,並且能夠連續的進行輸入輸出,正是由於流運算符的這些特性使得函數的返回值類型必須是引用類型,並且對於普通函數來講第一個參數也必須是引用類型。下面的例子說明了對流運算符的聲明和定義方法:設計模式

//普通流運算符函數模板
template<class LeftType, class RightType> LeftType& operator << (LeftType& left, const RightType& right) { //... return left; } template<class LeftType, class RightType> LeftType& operator >> (LeftType& left, RightType& right) {
    //...
    return left;
}

//類成員函數
class CA {
     public:

     template<class RightType> CA& operator << (const RightType& right) { //... return *this; } template<class RightType> CA& operator >>(RightType& right) {
        //...
        return *this;
    }
};

複製代碼

從上面的例子裏面能夠看出:數組

  • 流運算符的返回老是引用類型的,目的是返回值能夠作左值而且進行連續的流運算操做。
  • 對於輸入流運算符>>來講咱們要求右邊的參數必須是引用類型的,緣由就是輸入流會修改右邊參數變量的內容。若是右邊參數是普通的值類型則不會起到輸入內容被改變的效果。固然右邊參數類型除了採用引用以外,還能夠設置爲指針類型。
  • 對於輸出流運算符<<來講由於並不會改變右邊參數的內容,因此咱們建議右邊參數類型爲常量引用類型,目的是爲了防止函數內部對右邊參數的修改以及產生數據的副本或者產生多餘的構造拷貝函數的調用。
  • 通常對流運算符進行重載能夠採用普通函數也能夠採用類成員函數的形式。兩者的差異就是普通函數不能訪問類的私有變量。固然解決的方法是將普通函數設置爲類的友元函數便可。
2. 算術表達式運算符
描述
運算符種類 + - * / % ^ & | ~ >> <<
是否支持類成員 YES
是否支持普通函數 YES
運算單元 除~是一元以外其餘都是二元
返回類型 普通值類型

算術表達式是最多見的數學運算符號,上面分別定義的是加(+)、減(-)、乘(*)、除(/)、取餘(%)、異或(^)、與(&)、或(|)、非(~)、算術右移(>>)、邏輯左移(<<)幾個運算符。除~運算符外其餘運算符都是二元運算符,並且運算的結果和原來的值無關,而且不能作左值引用。下面是這些運算符重載的例子代碼:bash

//普通算術運算符函數模板
template<class ReturnType, class LeftType, class RightType> ReturnType operator + (const LeftType& left, const RightType& right) {
    //...
    return 返回一個ReturnType類型的值
}

//取反運算符是一個一元運算符。
template<class ReturnType, class LeftType>
ReturnType operator ~(const LeftType& left)
{
    //...
   return 返回一個ReturnType類型的值
}

//類成員函數
class CA
{
     public:

     template<class ReturnType, class RightType>
      ReturnType operator + (const RightType& right) const
     {
           //...
            return 一個新的ReturnType類型對象。
      }

      //取反運算符是一個一元運算符。
     template<class ReturnType>
      ReturnType operator  ~ () const
     {
           //...
            return 一個新的ReturnType類型對象。
      }
};

複製代碼

從上面的例子能夠看出:閉包

  • 函數的返回都是普通類型而不是引用類型是由於這些運算符計算出來的結果都和輸入的數據並非相同的對象而是一個臨時對象,所以不能返回引用類型,也就是不能再做爲左值使用。
  • 正是由於返回的值和輸入參數是不一樣的對象,所以函數裏面的入參都用常量引用來表示,這樣數據既不會被修改又能夠減小構造拷貝的產生。
  • 函數的返回類型能夠和函數的入參類型不一致,但在實際中最好是全部參數的類型保持一致。
  • 除了~運算符是一元運算符外其餘的都是二元運算符,你能夠看到上面的例子裏面一元和二元運算符定義的差別性。
  • 這裏面的<<和>>分別是表示位移運算而不是流運算。因此能夠看出其實咱們能夠徹底來自定義運算符的意義,也就是實現的結果能夠和真實的數學運算符的意義徹底不一致。
3. 算術賦值表達式運算符
描述
運算符種類 += -= *= /= %= ^= &= |= >>= <<=
是否支持類成員 YES
是否支持普通函數 YES
運算單元 二元
返回類型 左值引用

算術賦值表達式除了具備上面說的算術運算的功能以外,還有保存結果的做用,也就是會將運算的結果保存起來。所以這種運算符函數的第一個參數必須是引用類型,而不能是常量,同時返回類型要和第一個參數的類型一致。下面的例子說明了運算符的聲明和定義方法:函數

//普通運算符函數模板
template<class LeftType, class RightType> LeftType& operator += (LeftType& left, const RightType& right) {
    //...
    return left;
}

//類成員函數
class CA {
     public:

     template<class RightType> CA& operator += (const RightType& right) {
           //...
            return *this;
      }

     template<class RightType> CA& operator +=(RightType& right) {
        //...
        return *this;
    }
};

複製代碼

從上面的例子裏面能夠看出:ui

  • 算術賦值運算符的返回老是引用類型,並且要和運算符左邊的參數類型保持一致。
  • 函數的右邊由於並不會改變右邊參數的內容,因此咱們建議右邊參數類型爲常量引用類型,目的是爲了防止函數內部對右邊參數的修改以及產生數據的副本或者產生多餘的構造拷貝函數的調用。
4. 比較運算符
描述
運算符種類 == != < > <= >= && || !
是否支持類成員 YES
是否支持普通函數 YES
運算單元 除!外其餘的都是二元
返回類型 bool

比較運算符主要用於進行邏輯判斷,返回的是bool類型的值。這些運算符並不會改變數據的內容,所以參數都設置爲常量引用最佳。下面的例子說明了運算符的聲明和定義方法:

//普通算術運算符函數模板
template<class LeftType, class RightType> bool operator == (const LeftType& left, const RightType& right) {
    //...
    return true or false
}

//非運算符是一個一元運算符。
template<class LeftType>
bool  operator !(const LeftType& left)
{
    //...
   return true  or false
}

//類成員函數
class CA
{
     public:

     template<class RightType>
      bool operator == (const RightType& right) const
     {
           //...
            return true or false
      }

      //取反運算符是一個一元運算符。
      bool operator  ! () const
     {
           //...
            return true or false
      }
};

複製代碼

從上面的例子能夠看出:

  • 條件運算符返回的通常是固定的bool類型,由於不會改變數據的值因此不管參數仍是成員函數都用常量來修飾。
5. 自增自減運算符
描述
運算符種類 ++ --
是否支持類成員 YES
是否支持普通函數 YES
運算單元 一元
返回類型 普通類型,和左值引用

自增和自減運算符都是一元運算符,並且都會改變自身的內容,所以左邊參數不能是常量而只能是引用類型。又由於自增分爲後綴i++和前綴++i兩種形式(自減也同樣,下面就只舉自增的例子了)。後綴自增返回的值不能作左值而前綴自增返回的值則能夠作左值。爲了區分前自增和後自增,系統規定對前綴自增的運算符函數上添加一個int類型的參數做爲區分的標誌。下面的例子說明了運算符的聲明和定義方法:

//普通函數運算符函數模板

//++i
template<class LeftType> LeftType& operator ++ (LeftType& left, int) {
    //...
    return left
}

//i++
template<class LeftType>  
LeftType operator ++ (LeftType& left)
{
    //...
    return 新的LeftType值
}

//類成員函數
class CA
{
     public:

     CA& operator  ++ (int)
     {
           //...
            return *this;
      }

    CA operator ++ ()
     {
           //...
            return 新的CA類型值
      }
};

複製代碼

從上面的函數定義能夠看出:

  • 自增自減函數的參數以及返回值以及函數修飾都不能帶const常量修飾符。
  • 前綴自增的返回是引用類型能夠作左值,然後綴自增的返回類型則是值類型不能作左值。
  • 參數中有int聲明的是前綴自增而沒有int聲明的是後綴自增。
6.賦值運算符
描述
運算符種類 =
是否支持類成員 YES
是否支持普通函數 NO
運算單元 二元
返回類型 左值引用

賦值運算符只能用於類的成員函數中不能用於普通函數。賦值運算符重載的目的是爲了解決對象的深拷貝問題。咱們知道C++中對於對象賦值的默認處理機制是作對象內存數據的逐字節拷貝,這種拷貝對於只有值類型數據成員的對象來講是沒有問題的,可是若是對象中保存有指針類型的數據成員則有可能會出現內存重複釋放的問題。好比下面的代碼片斷:

class CA {
     public:
          int *m_a;
    
   ~CA(){ delete m_a;} 
};

void main() {
      CA a, b;
      a.m_a = new int;
      b = a;  //這裏執行賦值操做,可是有危險!
}

複製代碼

上面的代碼能夠看出當a,b對象的生命週期結束後的析構函數都會釋放數據成員的m_a所佔用的內存,可是由於咱們的默認對象賦值機制將會致使這部份內存被釋放兩次,從而產生了崩潰。所以在這種狀況下咱們就需對類的賦值運算符進行重載來解決對象的淺拷貝問題。上面的狀況除了要對一個類的賦值運算符進行重載外還有爲這個類創建一個拷貝構造函數。這裏面有一個著名的構造類的大三原則

若是一個類須要任何下列的三個成員函數之一,便三者所有要實現, 這三個成員函數是:拷貝構造,賦值運算符,析構函數. 實踐中,不少類只要遵循"大二規則"便可,也就是說只要實現拷貝構造,賦值操做符就能夠了,析構函數並不老是必需的.

實現大三原則的目的主要解決深拷貝的問題以及解決對象中有的數據成員的內存是經過堆分配創建的。在這裏拷貝構造函數的實現通常和賦值運算符的實現類似,兩者的區別在於拷貝構造函數通常用在對象創建時的場景,好比對象類型的函數參數傳遞以及對象類型的值的返回都會調用拷貝構造,而賦值運算符則用於對象創建後的從新賦值更新。好比下面的代碼:

class CA {
       //... 
   };

CA foo(CA a) {
    return a;
}

void main() {
     CA  a, c;   //構造函數
     CA b = foo(a);   //a在傳遞給foo時會調用拷貝構造,foo在返回數據給b時也會調用拷貝構造,即便這裏出現了賦值運算符。
     c = b;       //賦值運算符

}   

複製代碼

上面的代碼你能夠清楚的看到構造函數、拷貝構造函數、賦值運算符函數調用的時機和差別。下面咱們來對賦值運算符以及大三原則進行定義:

class CA {
             public:
               CA(){}   //構造函數
               CA(const CA& other){}  //拷貝構造
               CA& operator =(const CA& other)  //賦值運算符重載
               {
                    //..
                     return *this;
               }
              ~CA(){}   //析構函數
     }

複製代碼

從上面的定義能夠看出:

  • 賦值運算符要求返回的是類的引用類型,由於賦值後的結果是能夠作左值引用的。
  • 賦值運算符函數參數是常量引用代表不會修改入參的值。
7. 下標索引運算符
描述
運算符種類 []
是否支持類成員 YES
是否支持普通函數 NO
運算單元 二元
返回類型 引用

咱們知道在數組中咱們能夠經過下標索引的方式來讀取和設置某個元素的值好比:

int array[10] = {0};
     int a = array[0];
     array[0] = 10;
複製代碼

在實際中咱們的有些類也具有集合的特性,咱們也但願獲取這個集合類中的數據元素經過下標來實現,爲了解決這個問題咱們能夠對在類中實現下標索引運算符。這個運算符只支持在類中定義,而且索引的下標通常是整數類型,固然你能夠定義爲其餘類型以便實現相似於字典或者映射表的功能。具體的代碼以下:

class CA
     {
          public:
                 //只用於常量對象的讀取操做
                 template<class ReturnType,  class IndexType>
                 const ReturnType& operator [](IndexType index) const
                 {
                       return 某個returnType的引用
                  }
 
                 //用於通常對象的讀取和寫入操做
                 template<class ReturnType,  class IndexType>
                 ReturnType& operator[](IndexType index)
                 {
                        return 某個returnType的引用
                  }
      }    

複製代碼

從上面的代碼能夠看出:

  • 這裏定義了兩個函數主要是前者爲常量集合對象進行下標數據讀取操做,然後者則爲很是量集合對象進行下標數據讀取和寫入操做。
  • 這裏返回的不是值類型而是引用類型的目的是爲了減小由於讀取而產生沒必要要的內存複製。而寫入操做則必須使用引用類型。
8. 類型轉換運算符
描述
運算符種類 各類數據類型
是否支持類成員 YES
是否支持普通函數 NO
運算單元 一元
返回類型 各類數據類型

在實際的工做中,咱們的有些方法或者函數只接受特定類型的參數。而對於一個類來講,若是這個類的對象並非那個特定的類型那麼就沒法將這個對象做爲一個參數來進行傳遞,爲了解決這個問題咱們必需要爲類構建一個特殊的類型轉換函數來解決這個問題好比:

void foo(int a){
     cout << a << endl;
}

class CA {
   private:
        int m_a;
    public:
        CA(int a):m_a(a){}
       int toInt() {
            return m_a;
       }
};

void main() {
       CA a(10);
       
       foo(a);  // wrong!!! a是CA類型而非整數,編譯時報錯。
       foo(a.toInt());  // ok!! 
}

複製代碼

能夠看出爲了進行有效的參數傳遞,CA類必需要創建一個新的函數toInt來獲取整數並傳遞給foo。而類型轉換運算符則能夠更加方便以及易讀的形式來解決這種問題,經過類型轉換運算符的重載咱們的代碼在進行參數傳遞時就再也不須要藉助多餘的函數來完成,而是直接進行參數傳遞。類型轉換運算符重載實際上是一種適配器模式的實現,咱們能夠經過類型轉換運算符的形式來實現不一樣類型數據的轉換和傳遞操做。類型轉換運算符重載的定義方法以下:

class CA {
        public:
               template<class Type> operator Type() {
                     return Type類型的數據。
               }
    };

複製代碼

從上面的代碼中能夠看出:

  • 類型轉換運算符重載是不須要指定返回類型的,同時也不須要指定其餘的入參,而只須要指定轉換的類型做爲運算符便可。
  • 類型轉換運算符重載是能夠用於任何的數據類型的,經過類型轉換運算符的使用咱們就能夠很簡單的解決這種類型不匹配的問題了,下面的代碼咱們來看經過類型轉換運算符重載的解決方案:
class CA {
   private:
        int m_a;
    public:
        CA(int a):m_a(a){}
       operator int() {
            return m_a;
       }
};

void main() {
       CA a(10);
       foo(a);  // ok! 在進行參數傳遞是a會調用類型轉換運算符進行類型的轉換。
}

複製代碼
9. 函數運算符
描述
運算符種類 ()
是否支持類成員 YES
是否支持普通函數 NO
運算單元 N元
返回類型 任意

函數運算符在STL中的算法中被大量使用。函數運算符能夠理解爲C++對閉包的支持和實現。 咱們能夠經過函數運算符來將一個對象當作普通函數來使用,這個意思就是說咱們能夠在某些接收函數地址做爲參數的方法中傳遞一個對象,只要這個類實現的函數運算符而且其中的參數簽名和接收的函數參數簽名一致便可。咱們先來看下面一段代碼:

//定義一個模板fn他能夠接收普通函數也能夠接收實現函數運算符的對象
template<class fn> void foo2(int a, fn pfn) {
      int ret = pfn(a);
      std::cout << ret << std::endl;
}

int foo1(int arg) {
     return arg + 1;
}


class CA {
    private:
          int m_a;
     public:
      CA(int a):m_a(a){}
      //定義一個函數運算符
      int operator()(int arg) {
          return arg + m_a;
     }

    //定義另一個函數運算符
    void operator()(int arg1, int arg2) {
        std::cout << arg1 + arg2 + m_a << std::endl;
   }
};

void main() {
      foo2(10, &foo1);   //普通函數做爲參數傳遞。
    
     CA a(20);
     foo2(10, a);  //將對象傳遞給foo2當作普通函數來用。

    a(20, 30);    //這裏將對象當作一個普通的函數來用。
}


複製代碼

上面的代碼能夠看出來,由於CA類實現了2個函數運算符,因此咱們能夠將CA的對象當作普通的函數來用,在使用時就像是普通的函數調用同樣。咱們稱這種實現了函數運算符的類的對象爲函數對象。那麼爲何要讓對象來提供函數的能力呢?答案就是咱們能夠在對象的函數運算符內部訪問一些對象自己具備的其餘屬性或者其餘成員函數,而普通的函數則不具有這些特性。上面的例子也說明了這個問題,在類的函數運算符內部還可使用數據成員。一個類中可使用多個函數運算符的重載,並且函數運算符重載時的參數個數以及返回類型均可以徹底自定義。 咱們知道C++中不支持閉包機制,可是在某種程度上來講咱們能夠藉助函數運算符重載的方式來實現這種相似閉包的能力。

10. 復引用運算符、地址運算符、成員訪問運算符
描述
運算符種類 * & ->
是否支持類成員 YES
是否支持普通函數 除了* &支持外,->不支持
運算單元 1元
返回類型 任意

在C++語言中我能夠能夠對一個指針對象使用*運算符來實現取值操做,也就是獲得這個指針所指向的對象;對一個對象使用&運算符來獲得對象的指針地址;對於一個指針對象咱們可使用->運算符來訪問裏面的數據成員。所以這裏的*運算符表示的是取值運算符(也叫復引用運算符,間接引用運算符)、&表示的是取地址運算符、->表示的是成員訪問運算符。

class CA {
   public:
       int m_a;

};

void main() {
       CA a;

       CA *p = &a;   //取地址運算符
       cout << *p << endl;    //取值運算符
       p->m_a = 10;   //成員訪問運算符 

}
複製代碼

能夠看出來上面的三個運算符的主要目的就是用於指針相關的處理,也就是內存相關的處理。這三個運算符重載的目的主要用於智能指針以及代理的實現。也是是C++從語言級別上對某些設計模式的實現。在編程中有時候咱們會構造出一個類來,這個類的目的主要用於對另一個類進行管理,除了自身的一些方法外,全部其餘的方法調用都會委託給被管理類,這樣咱們就要在管理類中實現全部被管理類的方法,好比下面的代碼例子:

class CA {
    public:
       void foo1();
       void foo2();
       void foo3();
};

class CB {
   private:
       CA *m_p;
   public:
     CB(CA*p):m_p(p){}
     ~CB() { delete m_p;}  //負責銷燬對象

   CA* getCA(){ return m_p;}
   void foo1(){ m_p->foo1();}
   void foo2(){m_p->foo2();}
   void foo3(){m_p->foo3();}
};

void fn(CA*p) {
   p->foo1();
}

void main() {
    CB b(new CA);
     b.foo1();
     b.foo2();
     b.foo3();
    //由於fn只接受CA類型因此這裏CB要提供一個方法來轉化爲CA對象。
    fn(b.getCA());
   
}

複製代碼

上面的代碼能夠看出CB類是一個CA類的管理類,他會負責對CA類對象的生命週期的管理。除了這些管理外CB類還實現全部CA類的方法。當CA類的方法有不少時那麼這種實現的方式是低效的,怎麼來解決這個問題呢?答案就是本節裏面所說到的3個運算符重載。咱們來看如何實現這三個運算符的重載:

class CA {
    public:
       void foo1();
       void foo2();
       void foo3();
};

class CB {
   private:
       CA *m_p;
   public:
     CB(CA*p):m_p(p){}
     ~CB() { delete m_p;}  //負責銷燬對象

  public:
    //解引用和地址運算符是互逆的兩個操做
    CA&  operator *() { return *m_p;}
    CA*   operator &() {return m_p;}

    //成員訪問的運算符和&運算符的實現機制很是類似
    CA*  operator ->() { return m_p;}
 };


void fn1(CA*p) {
   p->foo1();
}

void fn2(CA&r) {
    r.foo2();
}

void main() {
    CB b(new CA);
     
     b->foo1();
     b->foo2();   //這兩個調用了->運算符重載

    fn1(&b);   //調用&運算符重載
    fn2(*b);   //調用*運算符重載
}


複製代碼

從上面的代碼能夠看出正是由於實現了對三個運算符的重載使得咱們不須要在CB類中重寫foo1-foo3的實現,以及咱們不須要提供特殊的類型轉換方法,而是直接經過運算符的方式就能夠轉化爲CA對象的並使用。固然一個完整的智能指針的封裝不只僅是對三個運算符的重載,咱們還須要對構造函數、拷貝構造、賦值運算符、類型轉化運算符、析構函數進行處理。若是你要想更加的瞭解智能指針就請去看看STL中的auto_ptr類

11. 內存分配和銷燬運算符
描述
運算符種類 new delete
是否支持類成員 YES
是否支持普通函數 YES
運算單元 N元
返回類型 new返回指針, delete不返回

是的,你沒有看錯C++中對內存分配new以及內存銷燬delete也是支持重載的,也就是說new和delete也是一種運算符。默認狀況下C++中的new和delete都是在堆中進行內存分配和銷燬,有時候咱們想對某個類的內存分配方式進行定製化處理,這時候就須要經過對new和delete進行重載處理了。而且系統規定若是實現了new的重載就必須實現delete的重載處理。關於對內存分配和銷燬部分我想單獨開闢一篇文章來進行詳細介紹。這裏面就只簡單了舉例如何來實現new和delete的重載:

class CA
{
    public:
     CA* operator new(size_t t){  return  malloc(t);}
     void operator delete(void *p) { free(p);}      
};
   
複製代碼

關於對new和delete運算符的詳細介紹請參考文章:C++的new和delete詳解

相關文章
相關標籤/搜索