运算符重载(加减运算符、前置加加(减减)后置加加(减减)运算符、赋值运算符、输入输出运算符、关系运算符、函数调用)
編譯器對于一個類會默認生成以幾種函數:
1.默認構造函數(空形參,空函數體)
2.默認拷貝構造函數(淺拷貝,也叫值拷貝、字節拷貝)
3.析構函數(空形參,空函數體。析構函數要求形參列表必須是空的,所以析構函數不能重載)
4.賦值函數(兩個對象賦值,也是以淺(值、字節)拷貝的形式)
講述以上這四種默認生成的函數的優秀博客點擊此處。
編譯器會對內置類型(比如char,int,short…)進行加減乘除運算符計算,但是我們自定義類型,有些運算編譯器不知道怎么做。我們自定義類型需要進行這些運算的時候,需要我們重載運算符。
運算符重載在不同情況下可以選擇用成員函數重載或者全局函數重載。本篇博客講述記錄以下幾種運算符重載:
1.加減運算符重載。
2.前置加加(減減)后置加加(減減)運算符重載。
3.賦值運算符重載。
4.左移右移運算符重載(輸入輸出運算符重載)。
5.關系運算符重載(大于、小于、等于、不等于)。
6.函數調用運算符重載(重載(),使用的時候也叫仿函數)。
注意:
(1)運算符重載依然是函數,而且本身就是重載函數,所以函數符重載函數仍然可以進行函數重載,且依然遵循函數重載的規則。
(2)".*"、"?:"、“sizeof”、"::"、"."這幾個運算符無法進行重載。
(3)不能對內置類型的運算符進行重載。即不能對int、double等類型的運算符進行重載。換句話說,運算符重載,參數必須至少有一個自定義類型。
(4)不能創建新的運算符。只能對已有的運算符進行重載,不能結合符號創建新的運算符。
一、加減運算符重載
運算符重載的統一函數名:oprerator“運算符”。
比如
重載+:返回類型 operator+(形參列表)
重載-:返回類型 operator-(形參列表)
…其他運算符重載函數的函數名類推。
運算符重載函數的返回類型很重要,要根據實際考慮而定。
1.加法運算符重載
(1)以全局函數的形式進行加法運算符重載
class A { public:A() {}A(int a, int b) :_a(a), _b(b) {} public:int _a;int _b; };//全局函數,”+“重載 A operator+(A a, A b) {A tmp;tmp._a = a._a + b._a;tmp._b = a._b + b._b;return tmp; } int main() {A a1(1, 2);A a2(9, 10);A a4 = operator+(a1, a2);//語句①A a5 = a1 + a1 + a2 + a2;//語句②cout << a4._a << " " << a4._b << endl;cout << a5._a << " " << a5._b << endl;return 0; }在這里選擇用全局函數重載"+“運算符的。下面再顯示用成員函數重載”+"運算符。
運行結果:
調用運算符重載函數的語句①是顯示的調用運算符重載函數,也是我們正常調用函數的寫法。語句②是簡便寫法方式。而且發現可以連續做加法。
(2)以成員函數的方式進行加法運算符重載
下面用成員函數重載"+"運算符。注意,成員函數有this指針,所以成員函數已經算是有一個參數了,加數是雙目運算符,在這里已經隱藏的this指針參數了,形參顯示的寫一個就好了。
代碼:
語句①是我們平常調用普通成員函數的寫法。
語句②是簡便寫法(一般都用簡便寫法調用運算符重載函數),真實調用方式和語句 ③一樣。
對象a5和對象a6是調用編譯器給的默認拷貝構造函數構造的。
運行結果:
2.減法運算符重載
不管是以成員函數方式重載還是以全局函數方式重載,減法運算符和加法運算符重載一摸一樣。把+改成-就好了。為了減少篇幅和重復代碼,這里就不粘貼修改后的代碼了。
二、前置加加(減減)后置加加(減減)運算符重載
1.前置加加(減減)運算符重載
(1)全局函數形式進行前置加加(減減)運算符重載
根據前面所提到的。前置++的運算符重載函數的形式應該是這種:返回類型 operator++(形參列表)。
考慮一下形參列表,如果用全局函數形式,那么肯定得把需要對其進行前置++的對象傳給函數,所以形參列表有一個參數,還有就是我們是對對象本身進行修改值操作,所以我們得以引用的形式傳進去。
考慮一下返回類型,考慮一下覺得以臨時對象的形式返回可以,以引用形式返回也行。隱隱覺得引用形式為返回類型更好,但是又不覺得哪里好。實際上一定要以引用形式返回。下面進行解釋。考慮好形參列表和返回類型,代碼就很好實現,先舉例以臨時對象的形式為返回類型:
代碼:
發現++(++a)錯誤。分析一下:
++(++a)相當于語句operator++(operator++(a))
operator++(a)返回的是一個臨時對象,臨時對象是一個存放在將要銷毀的空間中的對象。不允許對其進行操作,而且必須是以值拷貝的形式對臨時對象進行接收。所以把臨時對象當成右值也沒什么錯。對臨時對象再進行前置++是不允許的。所以我們返回類型不要以臨時對象的形式返回,我們要以引用形式返回,既然是模仿編譯器對內置類型的前置++,我們就得把操作做的像內置類型一樣。做高仿,我們是認真的。
修改后的代碼如下:
運行結果:
別說++(++a)了。一次性多進行幾次前置++都沒問題。
前置–和前置++一模一樣,把++改成–就好了。
(2)成員函數形式進行前置加加(減減)運算符重載
和加法(減法)運算符重載一樣考慮,作為成員函數,隱含this指針,而前置++只需要一個參數,所以this指針就夠了,成員函數形式我們不需要再顯示添加形參了。至于返回類型,和全局函數進行前置++運算符重載考慮一樣,以引用形式返回。
注意:如果我們對前置++進行了全局函數形式重載,同時又進行了成員函數形式重載,那么會讓編譯器調用前置++的時候不知道調用哪一個。所以對于同一種類型,不要讓它的前置++運算符重載既保留全局函數形式重載又保留成員函數形式重載。其他運算符也一樣要注意。
以成員函數形式進行前置加加運算符重載代碼:
運算結果:
后置–和后置++一模一樣,把++改成–就好了。
2.后置加加(減減)運算符重載
由于后置加加和前置加加沒辦法區分,因為形參列表一樣,都是只有一個參數。編譯器為了區分前置加加和后置加加,規定后置加加的形參列表要多一個占位形參int。這樣形參列表就不同了。所以后置加加的函數形式和前置加加的函數形式不同就在于形參多了int占位。
再來分析一下返回類型。我們需要對對象進行后置++,那么我們返回的肯定是++前的值。所以我們不能以引用形式返回。必須創建一個臨時對象保存要被后置++的對象的原始值。對原對象進行++操作,返回臨時對象。
(1)全局函數形式進行后置加加運算符重載
class A { public:A() {}A(int a, int b) :_a(a), _b(b) {}public:int _a;int _b; };A operator++(A& a, int) {A tmp = a;++a._a;++a._b;return tmp; }int main() {A a(1, 1);A b = a++;cout << a._a << " " << a._b << endl;cout << b._a << " " << b._b << endl;return 0; }運行結果:
以臨時對象返回,所以不能像前置++那樣進行連續后置++內置就算是內置類型也不能連續后置++。做高仿,我們是認真的。
后置–和后置++一模一樣,把++改成–就好了。
(2)成員函數形式進行前置加加(減減)運算符重載
像前面分析的一樣,把函數放到類里面就好了。成員函數有this指針。我們只需要把占位形參int寫到形參里面就好了。
運行結果:
成員函數進行后置–重載和后置++一模一樣,把++改成–就好了。
以上前置加加(減減)后置加加(減減)記錄完畢。
但是請看以下代碼,請先猜測一下結果:
。
。
。
。
運行結果如下:
三、賦值運算符重載
賦值運算符,涉及淺拷貝深拷貝問題。只要是賦值,就涉及深拷貝淺拷貝問題。淺拷貝是值拷貝,也叫字節拷貝。但是如果有在堆上申請空間的話,那么淺拷貝的后果就是多個指針指向這個堆空間,兩個對象共用一個堆空間。析構的話,就會存在重復釋放堆空間,發生崩潰。所以涉及堆空間的申請,一定要注意深淺拷貝問題。
1.普通賦值運算符重載
編譯器給的賦值運算符是淺拷貝。只對變量里面的值進行字節拷貝,即不論變量里面存放是普通值還是地址值,都會一個一個字節的拷貝賦值。而賦值運算符注意,必須得是成員函數。也就是不能以全局函數的形式對賦值運算符進行函數重載。(個人猜測,編譯器這樣限制是由于只允許我們賦值運算符只能存在同類的對象與同類的對象之間的賦值。不能讓兩個自定義類型之間存在賦值。)
由于有時候需要進行連續賦值操作,所以我們返回類型以引用方式返回。
運行結果:
注意:對于=運算符,編譯器從右邊執行,也就是先把a賦值給c再把c賦值給b。如果從左邊開始賦值的話,那么b得到的會是c的未初始化的值,c得到的是a的值。這樣連續賦值起不到作用。
2.需要深拷貝的賦值運算符重載
1.普通賦值運算符(淺拷貝)
為什么需要考慮深拷貝。我們先來看以下代碼:
賦值運算符重載函數有一句_ptr = b._ptr;
這句代碼將賦值語句的左對象的指針指向了右對象的堆空間。
析構函數有一句delete _ptr;
這句代碼對對象_ptr指針指向的堆空間進行釋放
主函數有兩句代碼:A c = a; b = a;
這兩句的代碼的效果一樣,都會使 c(b)的_ptr指針指向a的_ptr 指針指向的堆空間。
結果就是一個堆空間被多個對象的_ptr指針指向,對象的生存期到的時候,調用析構函數,堆空間第一次釋放沒問題,第二次釋放就會崩潰。解決這個問題的辦法就是我們進行深拷貝。深拷貝構造函數在拷貝構造函數里講過了。這里記錄賦值運算符重載解決問題。我們暫且叫深賦值淺賦值。(其實拷貝問題和賦值問題是同一種問題。)
2.賦值(深拷貝)運算符重載
代碼:
注意=運算符重載函數的代碼,給需要賦值的對象的_ptr指針分配了新空間。
所以兩個對象的_ptr指針指向的堆空間不同,調用析構函數時,不會出現對同一個堆空間釋放兩次的情況。運行結果如下:
四、輸入輸出運算符重載
重載之前,首先要知道輸入輸出的類型,輸出cout的類型為ostream(即output stream輸出流),輸入cin的類型為istream(input stream輸入流)。而且全局對象,只有一個。
分析以下輸出運算符重載。輸出運算符,就是全局對象調用operator<<構成的。cout就是全局對象。cout<<a本質就是operator<<(cout,a);所輸出運算符重載形式為:
返回類型 operator<<(ostream& out, const T& a);
其中,ostream是cout的類型,out相當于是cout的別名,T是需要輸出的對象的類型名,a是需要輸出的對象。
那么返回類型應該是什么?我們輸出的時候可以連續輸出。所以我們可以連續 調用這個輸出運算符,調用這個輸出運算符的是輸出流(ostream)cout,所以我們應該引用形式返回ostream類型。即:
ostream& operator<<(const ostream& out,const T& a);
那么就剩函數體了,下面給出代碼。
1.左移運算符重載
class A { public:A() {}A(int a, string str):_a(a),_str(str){} public:int _a;string _str; };ostream& operator<<(ostream& out, const A& a) {out << a._a << " " << a._str;return out; }int main() {A a(1, "aaa");A b(2, "bbb");cout << a << " " << b << endl;return 0; }運算結果:
上面是以全局函數的形式對輸出運算符進行重載。那么以成員函數的形式對輸出運算符進行重載呢?成員函數有this指針,所以對象不用顯示再寫了,所以輸出流ostream& out要顯示的寫到形參中。所以函數格式為:
ostream& operator<<(const ostream& out);
如下代碼:
寫出來之后,應該發現調用問題。對象調用,對象在前面,那么cout變成在后面了。調用就是以下形式:
int main() {A a(1, "aaa");A b(2, "bbb");a << cout;return 0; }可以輸出出來,但是肯定不合適。我們是做運算符重載,模仿編譯器的運算符操作方式,對自定義類型重載合適的運算符。模仿我們就要求高仿,使用方式得和編譯器對內置類型的運算符操作一樣。所以輸出運算符重載寫成成員函數的方式不合適。得寫成全局函數的方式。
一般類的屬性是私有,我們要用輸出重載函數輸出對象的私有屬性。所以一般我們都得把輸出運算符重載函數設置成類的友元函數。
2.右移運算符重載
在這里,我們把類屬性寫成私有,輸入輸出運算符重載都按規范要求寫成全局函數形式,且聲明為類的友元函數:
代碼:
運行結果:
五、關系運算符重載
對于關系運算符重載,返回類型肯定是bool類型的,這也是為什么不能連續比較大小關系的原因。雙目運算符,參數需要兩個。如果是以成員函數的方式進行關系運算符重載,由于隱含this指針參數,那么顯示的寫一個形參就好了。對于上面所講的那些運算符來說,關系運算符重載比較簡單。
1.大于號小于號運算符重載
(1)以全局函數的方式寫大于(小于)運算符重載
class A { public:A() {}A(int a, string str):_a(a),_str(str){} public:int _a;string _str; };bool operator>(const A& a, const A& b) {return a._a > b._a; }(2)以成員函數的方式寫大于(小于)運算符重載
class A { public:A() {}A(int a, string str):_a(a),_str(str){}bool operator>(const A& b){return this->_a > b._a;} public:int _a;string _str; };2.等于和不等于運算符重載
(1)以全局函數的方式寫等于(不等于)運算符重載
等于號的重載:
不等于號的重載:
class A { public:A() {}A(int a, string str):_a(a),_str(str){}public:int _a;string _str; };bool operator!=(const A& a, const A& b) {return a._a != b._a; }(2)以成員函數的方式寫等號(不等號)運算符重載
等號的運算符重載:
不等號運算符重載:
class A { public:A() {}A(int a, string str):_a(a),_str(str){}bool operator!=(const A& b){return this->_a != b._a;} public:int _a;string _str; };比較的標準是我們來定的,如果想要以其他標準(屬性)比較,我們就把函數體內換一下比較標準就好了。
六、函數調用運算符重載("()"重載,使用的時候被稱為法仿函數)
函數調用運算符重載是我在其他網站上看別人這樣稱呼的,我之前都成為小括號()重載,專業術語就應該叫作函數調用運算符重載。因為類調用這個重載函數的時候,和函數調用極其相似,所以也被稱為仿函數。當重載()時,不是創造了一種新的調用函數的方式,而是創造了一個可以傳遞任意數目參數的運算符函數。
注意:operator()必須是成員函數!
下面舉例()重載。
如下代碼:
以上代碼。我們對()進行了運算符重載。我們讓它返回比較大小的標準。即int類型的_a,這樣我們就不需要寫大于,小于,等于,不等于這些運算符重載函數了。寫了一個就可以直接比較大小等于不等于。調用()運算符重載的時候,以"對象名()"的形式,而調用函數的時候,是"函數名()"的形式。所以很像是函數調用。所以使用時也叫仿函數。
運行結果:
()運算符重載,功能很強大,我們可以用這個重載做很多想做的事情。比如說想要再做個給對象_a屬性加一個值的操作。
代碼:
運行結果:
總之,可以重載()做很多事情。函數參數不同為標準可以重載operator()。
此外,我們還可以對"[]"、"*“重載,這里不進行記錄了。但是都很容易寫,這些運算符重載都一樣。掌握兩三個就可以達到舉一反三的效果,對其他運算符重載也只是換碗不換藥。
只不過記住賦值運算符”=“和”()"運算符只能作為成員函數重載。輸入輸出運算符重載需要規范的寫成全局函數重載的方式。
總結
以上是生活随笔為你收集整理的运算符重载(加减运算符、前置加加(减减)后置加加(减减)运算符、赋值运算符、输入输出运算符、关系运算符、函数调用)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 数组指针和指针数组,函数指针和指针函数,
- 下一篇: C++类的继承