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

2023-04-18 12:01:42

0、引言

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

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

1、運運算元過載定義

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

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

1 <返回型別說明符> operator <運運算元符號>(<參數列>)
2 {
3      <函數體>
4 }

        其中,「返回型別說明符」指出過載運運算元的返回值型別,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 }

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

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 }

        執行結果為:

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)過載的運運算元必須和使用者定義的物件一起使用,運運算元引數(操作的物件)中至少應有一個是類物件(或類物件的參照)。


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