運算符重載
- 1.當運算符被用于類類型的對象時,C++語言允許我們通過運算符重載的形式指定新的含義。C++規定類類型對象使用運算符時,必須轉換成調用對應運算符重載,若沒有對應的運算符重載,則會編譯報錯。
- 2.?運算符重載是具有特殊名字的函數,他的名字是由operator和后面要定義的運算符共同構成。和其他函數一樣,它也具有其返回類型和參數列表以及函數體。
- 3.?重載運算符函數的參數個數和該運算符作用的運算對象數量一樣多。一元運算符有一個參數,二元運算符有兩個參數,二元運算符的左側運算對象傳給第一個參數,右側運算對象傳給第二個參數。
- 4.如果一個重載運算符函數是成員函數,則它的第一個運算對象默認傳給隱式的this指針,因此運算符重載作為成員函數時,參數比運算對象少一個。
- 5.運算符重載以后,其優先級和結合性與對應的內置類型運算符保持一致。
- 6.不能通過連接語法中沒有的符號來創建新的操作符:比如operator@。7.
- 7. .*? ? ?::? sizeof? ??:? ? . 注意以上5個運算符不能重載。(選擇題里面常考,大家要記一下)
- 8.重載操作符至少有一個類類型參數,不能通過運算符重載改變內置類型對象的含義,如:int operator+(int x, int y)
- 9. 一個類需要重載哪些運算符,是看哪些運算符重載后有意義,比如Date類重載operator-就有意義,但是重載operator*就沒有意義。
- 10.重載++運算符時,有前置++和后置++,運算符重載函數名都是operator++,無法很好的區分。C++規定,后置++重載時,增加一個int形參,跟前置++構成函數重載,方便區分。
- 11.重載<<和>>時,需要重載為全局函數,因為重載為成員函數,this指針默認搶占了第一個形參位置,第一個形參位置是左側運算對象,調用時就變成了對象<<cout,不符合使用習慣和可讀性。重載為全局函數把ostream/istream放到第一個形參位置就可以了,第二個形參位置當類類型對象。
?如代碼,自定義類型進行使用“==”操作符會產生報錯:
由于自定義類型比較復雜,所以默認情況下自定義類型是無法進行運算的,但是呢,有的自定義類型進行相關運算卻是有意義的,比如我們經常遇到的日期類,兩個日期相減得到的就是兩個日期之間相隔的天數,日期加上天數就能得到另一個日期,學習運算符重載,我們可以讓自定義類型進行運算,這樣會更加方便。
話不多說,我們直接來看代碼吧:
先寫好日期類的代碼,這是我們這一小節經常需要使用的類:
//日期類
#include<iostream>
using namespace std;
class Date
{
public:
//默認構造函數Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << '/' << _month << '/' << _day << endl;}
//在寫運算符重載的算法中,我們不可避免的會用到類中的成員變量,所以需要將成員變量改為公有的,但其實我們一般不推薦這么做,在以后我們學習了友元函數就有更好地解決辦法
//private:int _year;int _month;int _day;
};
現在,我們要對“==”運算符進行重載,也就是比較兩個日期是否相同,按照運算符重載的特點:
//重載運算符名字:由operator和后面要定義的運算符共同構成
//具有返回值,參數,函數體
bool operator==(Date x1 ,Date x2)
{//將自定義類型的比較轉化為成員變量中內置類型的比較return ((x1._year == x2._year) && (x1._month == x2._month) && (x1._day == x2._day));
}
寫完這個代碼后,再看是否還會有語法錯誤:
可以看到,當再次使用這個運算符時,就不會產生報錯了。
另外,我們可以想一下上面那個運算符重載的代碼還有啥可以改進的地方。我們在前面一節介紹過了,自定義類型的傳值傳參會調用拷貝構造,但是傳引用傳參就不會調用拷貝構造,所以在C++中,為了提高程序的性能,我們要習慣去使用引用傳參:
//引用傳參不需要調用拷貝構造
bool operator==(const Date& x1, const Date& x2)
{return ((x1._year == x2._year) && (x1._month == x2._month) && (x1._day == x2._day));
}
?那我們應當如何使用重載后的運算符呢?
方法一:函數調用的形式
bool ret= operator==( d1 , d2 );
方法二:就像內置類型一樣直接使用運算符,一般情況下我們推薦這種寫法
d1==d2;
現在我們就來運用一下這個運算符:
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void Print(){cout << _year << '/' << _month << '/' << _day << endl;}int _year;int _month;int _day;
};//引用傳參不需要調用拷貝構造
bool operator==(const Date& x1, const Date& x2)
{return ((x1._year == x2._year) && (x1._month == x2._month) && (x1._day == x2._day));
}
int main()
{Date d1(2025, 8, 3);Date d2(2025, 8, 8);if (d1 == d2){cout << "兩個日期相同" << endl;}else{cout << "兩個日期不同" << endl;}return 0;
}
另外,為了在運算符重載中能夠使用類里面的成員變量,我們將成員變量改為了公有的,但其實這種方式不太好,一種解決方法就是將運算符重載函數寫到類里面去,我們來試一下:
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}bool operator==(const Date& x1, const Date& x2){return ((x1._year == x2._year) && (x1._month == x2._month) && (x1._day == x2._day));}void Print(){cout << _year << '/' << _month << '/' << _day << endl;}
private:int _year;int _month;int _day;};//引用傳參不需要調用拷貝構造int main()
{Date d1(2025, 8, 3);Date d2(2025, 8, 8);return 0;
}
但是編譯器會報錯:
為啥就只是把運算符重載函數改到類里面就報錯了呢?
這就是this指針在裝神弄鬼了。表面上我們把函數寫到類里面去是傳遞了兩個參數,其實第一個參數的位置還有一個隱含的this指針。所以其實你是傳遞了3個參數的,但“==”只能接受2個操作數,所以會報錯。
還需注意的是,在 C++ 中,當運算符重載函數作為成員函數定義在類內部時,this
?指針指向的是運算符左側的操作數對象的地址。這是運算符重載的核心規則之一。(這也就是第4點的意思)
所以,我們在類里面規定運算符重載時,我們自己寫的參數個數應該比運算符實際能接受的操作數個數少一,比如,上面的代碼應該改成:
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
//編譯器會把它處理為:operator(Date*this,const Date& x2)bool operator==(const Date& x2){return ((_year == x2._year) && (_month == _month) && (_day == x2._day));}void Print(){cout << _year << '/' << _month << '/' << _day << endl;}
private:int _year;int _month;int _day;};//引用傳參不需要調用拷貝構造int main()
{Date d1(2025, 8, 3);Date d2(2025, 8, 8);d1 == d2;//等價于:d1.operator==(d2);if (d1 == d2){cout << "兩個日期相同" << endl;}else{cout << "兩個日期不同" << endl;}return 0;
}
?.*? ? ?::? sizeof? ??:? ? . 5個運算符不能重載,我們現在來簡單講一下.*運算符的用法(這個運算符的挺少用的,簡單看一下就好),見以下代碼和注釋:
//成員函數指針的創建與訪問:
#include<iostream>
using namespace std;
void func1()
{cout << "void func1()" << endl;
}
class A
{
public:void func2(){cout << "void func2()" << endl;}
};int main()
{//普通函數指針的創建:void (*pf1)() = func1;//利用函數指針來調用函數:(*pf1)();//類的成員函數指針的創建:void(A:: * pf2)() =& A::func2;//為啥類的成員函數指針要這樣寫://在 C++ 中,類成員函數指針的聲明和賦值需要特殊語法,這是由成員函數的本質特性決定的//成員函數(非靜態)與普通函數不同,它隱含一個 this 指針參數(用于訪問對象實例的數據)// 因此,成員函數指針的語法需要體現:所屬類(A::),調用時的對象綁定(通過 .* 或 ->* 運算符)//成員函數指針的調用
//想一下,利用成員函數的指針調用成員函數可以這么調用嘛:(*pf2)();//不可以的:因為成員函數中是有隱含的this指針的,this指針接收的是調用函數的對象的地址,所以在調用成員函數//時,還需要指定對象A aa;//利用成員函數指針調用函數:(aa.*pf2)();//這就是.*運算符的用途return 0;
}
再來講解一下特點8是啥意思:
重載操作符至少有一個類類型參數:意思是當你重載一個運算符(如?+
,?==
,?<<
?等)時,至少有一個參數必須是自定義的類(class)或結構體(struct)類型,而不能全部是基本類型(如?int
,?double
,?char
?等)。
這是因為:
C++ 不允許你修改基本類型(如?
int
,?float
?等)的運算符行為,否則會導致代碼混亂。運算符重載的目的是為了讓自定義類型(如?
Date
,?String
,?Vector
?等)也能像內置類型一樣使用運算符。
?不能通過運算符重載改變內置類型對象的含義:意思是?你不能改變基本類型(如?int
,?float
,?char
?等)的運算符的原始行為。
這是因為:
如果允許修改基本類型的運算符行為,比如讓?
1 + 1
?返回?3
,那代碼會變得極其混亂,無法維護。C++ 只允許你為自定義類型(如?
Date
,?String
)定義新的運算符行為,而不能篡改內置類型的運算規則。
?賦值運算符重載
賦值運算符重載是一個默認成員函數,用于完成兩個已經存在的對象直接的拷貝賦值,這里要注意跟拷貝構造區分,拷貝構造用于一個對象拷貝初始化給另一個要創建的對象。
賦值運算符重載的特點:
1.?賦值運算符重載是一個運算符重載,規定必須重載為成員函數。賦值運算重載的參數建議寫成const 當前類類型引用,否則會傳值傳參會有拷貝
2.?有返回值,且建議寫成當前類類型引用,引用返回可以提高效率,有返回值目的是為了支持連續賦值場景。
3.?沒有顯式實現時,編譯器會自動生成一個默認賦值運算符重載,默認賦值運算符重載行為跟默認拷貝構造函數類似,對內置類型成員變量會完成值拷貝/淺拷貝(一個字節一個字節的拷貝),對自定義類型成員變量會調用他的賦值重載函數。
4.?像Date這樣的類成員變量全是內置類型且沒有指向什么資源,編譯器自動生成的賦值運算符重載就可以完成需要的拷貝,所以不需要我們顯示實現賦值運算符重載。像Stack這樣的類,雖然也都是內置類型,但是_a指向了資源,編譯器自動生成的賦值運算符重載完成的值拷貝/淺拷貝不符合我們的需求,所以需要我們自己實現深拷貝(對指向的資源也進行拷貝)。像MyQueue這樣的類型內部主要是自定義類型Stack成員,編譯器自動生成的賦值運算符重載會調用Stack的賦值運算符重載,也不需要我們顯示實現MyQueue的賦值運算符重載。這里還有一個小技巧,如果一個類顯示實現了析構并釋放資源,那么他就需要顯示寫賦值運算符重載,否則就不需要。?
寫代碼來理解這些特點:
#include<iostream>
using namespace std;
class Date
{
public://構造函數Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//拷貝構造:Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}//賦值運算符重載:如果是傳值傳參,在調用賦值運算符重載時還會調用拷貝構造//但這里是傳引用傳參,不會調用拷貝構造void operator=(const Date& d){_year = d._year;_month = d._month;_day = d._day;}void Print(){cout << _year << '/' << _month << '/' << _day << endl;}
private:int _year;int _month;int _day;};//引用傳參不需要調用拷貝構造int main()
{Date d1(2025, 8, 3);
//拷貝構造Date d2(d1);//賦值構造Date d3(2025, 7, 8);d3 = d1;d1.Print();d2.Print();d3.Print();return 0;
}
在調試過程中,確實調用了賦值運算符重載:?
?還需要區分一個點,如果在main函數中有這樣一行代碼:
Date d4 = d1;
請問這個代碼是會調用拷貝構造還是賦值運算符重載?
答案是賦值運算符重載,可以看到d4這個對象是在創建的時候同時讓對象d1對其進行初始化,這就是拷貝構造的另一種寫法,可能容易與拷貝構造混淆,這一點在上一節我們也是講到過的哦。
看到這里,就有一個注意的地方,我們上面寫的賦值運算符重載是有一點小小的錯誤的哦,特點2已經告訴我們了,賦值運算符重載是有返回類型的哦,這樣才能支持連續賦值。
那么我們可以想一下:如果有兩個日期類對象d1和d3,我們要執行d1=d3,那么賦值運算符重載的返回值是什么呢?返回的應當是d1中的內容,因為是要把d3賦值給d1
//執行:d1=d3//等價于: d1.operator(d3)//形參部分:operator=(const Date& d)//實際上,編譯器會將代碼轉化為:operator=(Date* this , const Date& d)//實參的傳參部分:d1.operator(&d1 , d3 )//函數體內部://{// this-> _year = d._year;// this-> _month = d._month;// this->_day = d._day;//}//最后返回值返回的應該是d1這個對象,而在參數中,d1這個對象的地址實際上傳給了this,所以可以通過*this獲得d1
//最終代碼:Date& operator=(const Date& d){_year =d._year;_month = d._month;_day = d._day;return *this;}
//為什么這里可以用引用返回
//原因一:因為*this并不是局部對象,*this得到的就是d1,他是在main函數中定義的,出了賦值運算符重載的作用域以后*this對應的空間并沒有被銷毀,所以這里可以傳引用返回
//原因二:如果這里是傳值返回,就要再調用拷貝構造函數,這樣比較麻煩
看到這里,其實代碼還可以再修改一下,想一下,假設有一種情況,有的小伙伴執行:d1=d1這種自己給自己賦值的代碼(雖然這種代碼無意義,但難免會有人真這么做),我們就可以把代碼改成這樣:
Date& operator=(const Date& d)
{
//當自己給自己復制時,this表示的是d1的地址,d是d1的別名,&d也就是&d1
//即this==&d1if(this!=&d){_year =d._year;_month = d._month;_day = d._day;}return *this;
}
日期類的實現
上面講了這么多,我們就一起來實踐一下,來完成日期類的實現吧!!!
//Date.h
#pragma once
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;class Date
{
public://構造函數的聲明Date(int year = 1900, int month = 1, int day = 1);//打印函數的聲明void Print();
//日期的相關比較函數bool operator<(const Date& d);bool operator<=(const Date& d);bool operator>(const Date& d);bool operator>=(const Date& d);bool operator==(const Date& d);bool operator!=(const Date& d);// d1 += 天數Date& operator+=(int day);Date operator+(int day);// d1 -= 天數Date& operator-=(int day);Date operator-(int day);// d1 - d2int operator-(const Date& d);// ++d1 -> d1.operator++()Date& operator++();// d1++ -> d1.operator++(0)// 為了區分,構成重載,給后置++,強行增加了一個int形參// 這里不需要寫形參名,因為接收值是多少不重要,也不需要用// 這個參數僅僅是為了跟前置++構成重載區分Date operator++(int);Date& operator--();Date operator--(int);//得到一月有多少天:int GetDayOfMonth(int year,int month){int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };if (month == 2 && ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0))){arr[month]++;}return arr[month];}private:int _year;int _month;int _day;
};//Date.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
//注意:全缺省類默認構造函數的缺省參數只在聲明中寫,不在定義中寫
Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;
}void Date::Print()
{cout << _year << '/' << _month << '/' << _day << endl;
}bool Date::operator==(const Date& d)
{return ((_year == d._year) && (_month == d._month) && (_day == d._day));
}bool Date::operator!=(const Date& d)
{return !(*this == d);
}
bool Date::operator>=(const Date& d)
{return !(*this < d);
}
bool Date::operator<=(const Date& d)
{return !(*this > d);
}
bool Date::operator< (const Date & d)
{return (_year < d._year) ||((_year == d._year) && (_month < d._month)) ||((_year == d._year) && (_month == d._month) && (_day < d._day));
}
bool Date::operator> (const Date& d)
{return (*this != d) && (!(*this < d));
}Date& Date::operator+=(int day)
{_day += day;while (_day > GetDayOfMonth(_year, _month)){_day -= GetDayOfMonth(_year, _month);_month++;if (_month == 13){_year++;_month = 1;}}return *this;
}
//不改變*this
Date Date::operator+(int day)
{Date d1 = *this;d1 += day;return d1;
}
//
//// d1 -= 天數
Date& Date:: operator-=(int day)
{if (_day > day){_day -= day;return *this;}while (_day <= day){_month--;if (_month == 0){_year--;_month = 12;}_day += GetDayOfMonth(_year, _month);if (_day > day){_day -= day;break;}}return *this;
}
Date Date::operator-(int day)
{//Date d(*this);Date d = *this;d -= day;return d;
}
////后置加加:有拷貝構造
Date Date::operator++(int)
{//Date d(*this);Date d = *this;(*this) += 1;return d;
}
//前置--
Date& Date:: operator--()
{*this -= 1;return *this;
}
Date Date::operator--(int)
{//Date d(*this);Date d = *this;(*this) -= 1;return d;
}
//前置++:沒有拷貝構造
Date& Date::operator++()
{*this += 1;return *this;
}
////
//// d1 - d2
int Date::operator-(const Date& d)
{//暴力搜索Date d1 = d;assert(*this > d);int count = 0;while (*this != d1){d1++;count++;}return count;
}// d1 += 100
//Date& Date::operator+=(int day)
//{
// *this = *this + day;
// return *this;
//}
//
//// d1 + 100
//Date Date::operator+(int day)
//{
// Date tmp(*this);
//
// tmp._day += day;
// while (tmp._day > GetMonthDay(tmp._year, tmp._month))
// {
// tmp._day -= GetMonthDay(tmp._year, tmp._month);
// ++tmp._month;
// if (tmp._month == 13)
// {
// ++tmp._year;
// tmp._month = 1;
// }
// }
//這是+和+=的另外一套寫法,上面一種寫法是讓+去復用+=的邏輯,會經過2次復制拷貝
//這一種寫法是讓+=去復用+,會經過3次拷貝
//所以我們選用上一種方法:讓+去復用+=//test.cpp
#include"Date.h"
#include<iostream>
using namespace std;
int main()
{Date d1(2025, 1, 1);//Date d2(d1);//Date d3 = d2 + 100;//d3.Print();//d1 -= 100;//d1.Print();//Date d2 = d1 - 100;//d2.Print();/*++d1;*///d1.Print();//Date d2=d1++;//d1.Print();//d2.Print();//Date d2=d1--;//d1.Print();//d2.Print();//Date d2=--d1;//d1.Print();//d2.Print();Date d2(2024, 9, 27);Date d3 = d2 + 96;d3.Print();int gap = d1 - d2;cout << gap << endl;return 0;
}