C++過載的奧義之運算子過載

Sharemaker發表於2023-04-18

0、引言

        過載,顧名思義從字面上理解就是重複裝載,打一個不恰當的比方,你可以用一個籃子裝蔬菜,也可以裝水果或者其它,使用的是同一個籃子,但是可以用籃子重複裝載的東西不一樣。

        正如在之前的文章《過載的奧義之函式過載》中介紹的類似,函式的過載是指利用相同的函式名設計一系列功能相近,但是功能細節不一樣的函式介面;因此運算子過載也是指對於同一個運算子來說,它可以用於實現不同的功能。下面就一起來理解下運算子過載的應用。

1、運算子過載定義

        正常來說,我們一般使用的運算子是對基本的資料型別進行操作,但是在C++中有了物件,導致物件無法透過運算子進行運算,故引入了運算子過載即需要重新的定義這些運算子,賦予已有運算子新的功能,使它能夠用於特定型別執行特定的操作。運算子過載的實質是函式過載,它提供了C++的可擴充套件性。

        運算子過載是透過建立運算子函式實現的,運算子函式定義了過載的運算子將要進行的操作。運算子函式的定義與其他函式的定義類似,唯一的區別是運算子函式的函式名是由關鍵字operator和其後要過載的運算子符號構成的。運算子函式定義的一般格式如下:

1 <返回型別說明符> operator <運算子符號>(<參數列>)
2 {
3      <函式體>
4 }
C++過載的奧義之運算子過載

        其中,“返回型別說明符”指出過載運算子的返回值型別,operator是定義運算子過載函式的關鍵字,“運算子符號”指出要過載的運算子名字,是C++中可過載的運算子,比如要過載加法運算子,這裡直接寫“+”即可,“參數列”指出過載運算子所需要的引數及其型別。可以看出,運算子過載是一種形式C++多型的體現。

        例如,使用“+”將兩個物件相加,編譯器將根據運算元的數目和型別決定使用哪種加法定義,這樣可以讓程式碼看起來更加自然。

 1 //正常情況下兩個陣列的數相加
 2 for(int i= 0; i<10; i++)
 3     c[i] = a[i] + b[i];
 4 //可以透過定義一個陣列的類,過載“+”運算子後
 5 //隱藏了內部機制,並強調了實質
 6 arry operator+(arry p,arry q)
 7 {
 8    arry t;
 9     for(int i= 0; i<10; i++)  //c = a + b; 
10     {  
11       t.a[i]=p.a[i]+q.a[i];
12     }
13     return t;
14 }
C++過載的奧義之運算子過載

        運算子過載就是對已有的運算子重新進行定義,賦予其另一種功能,以達到適應不同的資料型別。運算子過載不能改變它本來的寓意(也就是加法不能變更為減法),運算子過載只是一種 “語法上的方便” ,它只是一種函式呼叫的方式。

2、作為成員函式進行過載

        我們就以“+”運算子過載舉例:

 1 #include <iostream>
 2 using namespace std;
 3 class addfloat
 4 {
 5 public:
 6     addfloat(float p);
 7     //宣告運算子過載
 8     addfloat operator+(const addfloat &A) const;
 9     void show() const;
10 private:
11     float m_p;  
12 };
13 addfloat::addfloat(float p)
14 {
15     m_p = p;
16 }
17 //作為類的成員函式實現運算子過載
18 addfloat addfloat::operator+(const addfloat &A) const
19 {
20     addfloat B;
21     B.m_p = this->m_p + A.m_p;
22     return B;
23 }
24 void addfloat::show() const
25 {
26     cout<<"輸出結果是"<<m_p<<endl;
27 }
28 
29 
30 int main()
31 {
32     addfloat m(5.1);
33     addfloat n(1.5);
34     addfloat t;
35     t = m + n; //兩個addfloat類物件相加:t = m.operator+(n);
36     t.show();
37     return 0;
38 }
C++過載的奧義之運算子過載

        執行結果為:

1 輸出結果是6.6

        從上面的例子可以看出,在addfloat類中對“+”運算子進行了過載 ,過載後可以對該類的物件進行加法運算。當執行 t = m + n時,編譯器檢測到“+”左邊的m(“+”具有左結合性,所以先檢測左邊)是一個 addfloat類物件,就會呼叫成員函式 operator+(),將表示式轉換成如下格式:

        t = m.operator + (n);

        表示式中m作為呼叫函式的物件,n作為函式的實參。

3、作為全域性函式進行過載

        對於之前的例子:t = m + n,m和n是作為addfloat類的物件進行相加的,使用成員函式 operator+()轉換為了t = m.operator+(n),如果n不是類的物件,而是一個常數,例如:

        t = m + 5.2;那麼可以轉換t = m.operator+(5.2);

        但是如果m是一個常數時,即:t = 5.2 + n;則t = (5.2).operator + (n)這種轉換是不允許的,編譯器會報錯,因為5.2不能作為類的物件呼叫運算子過載operator+()函式。

        這種場景下針對“+”這種運算子作為類的成員函式進行過載是不可以的。運算子過載不僅僅可以透過類的成員函式來實現,也可以透過全域性函式來實現。

        我們需要將運算子過載的全域性函式宣告為友元函式,因為大多數時候過載運算子要訪問類的私有資料,(當然也可以設定為非友元非類的成員函式,但是非友元又不是類的成員函式,是沒有辦法直接訪問類的私有資料的),如果不宣告為類的友元函式,而是透過在此函式中呼叫類的公有函式來訪問私有資料會降低效能。所以一般都會設定為類的友元函式,這樣我們就可以在此非成員函式中訪問類中的資料了。

 1 #include <iostream>
 2 using namespace std;
 3 class addfloat
 4 {
 5 public:
 6     addfloat(float p);
 7     //宣告為友元函式
 8     friend addfloat operator+(const addfloat &A, const addfloat &B);
 9     void show() const;
10 private:
11     float m_p;  
12 };
13 addfloat::addfloat(float p)
14 {
15     m_p = p;
16 }
17 
18 void addfloat::show() const
19 {
20     cout<<"輸出結果是"<<m_p<<endl;
21 }
22 
23 //作為全域性函式進行過載
24 addfloat operator+(const addfloat &A, const addfloat &B)
25 {
26     addfloat C;
27     C.m_p = A.m_p + B.m_p;
28     return C;
29 }
30 
31 int main()
32 {
33     addfloat m(5.1);
34     addfloat n(1.5);
35     addfloat t;
36     t = m + n; //兩個addfloat類物件相加:t = m.operator+(n);
37     t.show();
38     return 0;
39 }

        由上述程式可以看出,運算子過載函式operator+()不是 addfloat類的成員函式,但是卻用到了 addfloat類的 private 成員變數m_p,所以需要在 addfloat類中將operator+()函式宣告為友元函式。

        當執行t = m + n時,編譯器檢測到“+”兩邊都是addfloat類的物件,就會轉換為類似下面的函式呼叫:

        t = operator + (m, n);

        因此,m和n都可以看作是函式的實參:

        t = m + 5.2轉換為 t = operator + (m, 5.2);

        t = 5.2 + n轉換為 t = operator + (5.2, n);

        以全域性函式的形式過載“+”,是為了保證“+”運算子的運算元能夠被對稱的處理;換句話說,常數在“+”左邊和右邊都是正確的;

        因此,運算子左右兩邊都有操作物件時,且這兩個操作物件可以互換,最好可以使用全域性函式的形式過載,例如:+、-、*、/、==、!= ,這些符合運算子兩邊有操作物件的運算子。

4、運算子過載的一些規則

(1)可以過載的運算子

(2)不可以過載的運算子

.         (成員訪問運算子)

.*       (成員指標訪問運算子)

::        (域運算子)

sizeof (長度運算子)

?:        (條件運算子)

(3) 只能以成員函式的形式過載的運算子(與 this關聯太多)

=         (賦值運算子)

()         (函式呼叫運算子)

[]         (下標運算子)

->       (成員訪問運算子)  

(4)只能以全域性函式過載的運算子

<<      (左移運算子)  

>>      (右移運算子)  

(5)運算子過載函式既可以作為類的成員函式,也可以作為全域性函式。友元函式運算子過載函式與成員運算子過載函式的區別是:友元函式沒有this指標,而成員函式有,因此,在兩個運算元的過載中友元函式有兩個引數,而成員函式只有一個。

(6)有一部分運算子過載既可以是成員函式也可以是全域性函式,雖然沒有一個必然的、不可抗拒的理由選擇成員函式,但我們應該優先考慮成員函式,這樣更符合運算子過載的初衷。

(7)對於複合的賦值運算子如 +=、-=、*=、/=、&=、!=、~=、%=、>>=、<<= 建議過載為成員函式;

單目運算子最好過載為成員函式;

對於其它運算子,建議過載為全域性函式。

(8)使用運算子不能違反運算子原來的語法規則,原來有幾個運算元、運算元在左邊還是在右邊,這些都不會改變。算符過載函式不能有預設的引數,否則就改變了運算子運算元的個數。

(9)運算子的優先順序不能被過載改變。然而,圓括號能夠強制改變表示式中過載運算子的求值順序。

(10)運算子的結合性不能被過載改變。如果一個運算子的結合性是從左向右,那麼,它的所有過載的版本的結合性依然是從左向右

(11)不能創造新的運算子,即只能過載現有的運算子。例如不能定義operator** (···)來表示求冪。

(12)過載的運算子必須和使用者定義的物件一起使用,運算子引數(操作的物件)中至少應有一個是類物件(或類物件的引用)。


↓↓↓更多技術內容和書籍資料獲取,入群技術交流敬請關注“明解嵌入式”↓↓↓

相關文章