【C++从练气到飞升】05---运算符重载(一)+https://developer.aliyun.com/article/1502590
四、日期类的实现
下面日期类的实现,是通过定义和声明分离来写的所以需要加Date::
1. 重载关系运算符
对于关系运算符有以下几种<、== 、<=、>、>=、!=,因为它们之间存在的逻辑关系,可以通过复用来实现,就比如:想要知道一个数a是否>另一个数b就可以通过判断a是否<=b来实现,所以只需要写一个小于和等于的逻辑之后的复用即可
重载<运算符:
bool Date::operator<(const Date& d) { if (_year < d._year) { return true; } else if (_year == d._year && _month < d._month) { return true; } else if (_year == d._year && _month == d._month && _day < d._day) { return true; } else { return false; } }
重载==运算符:
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 || *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 !(*this == d); }
2. 完善日期类
对于日期类的计算,若想知道100天以前或是100天以后是哪一天是非常有价值的,但是一个日期类型和一个整型是可以相加相减吗?当然是可以的,可以通过重载运算符来实现。
获取每个月份的天数:
int Date::GetMonthDay(int year, int month) { static : GetMonthDay肯定会重复调用,每次调用还要定义一遍这个数组会有一定的消耗 const static int monthArray[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if (month == 2&&(((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))) { return 29; } return monthArray[month]; 返回的是数组中这个值的临时拷贝 }
注意:对于2月是分闰年和平年的,但是其他月份是固定不变的,所以可以通过一个数组来存放每个月的天数,并且以下标作为每个月的月份所以不是monthArray[12],而是monthArray[13]。其中要把数组定义为静态数组目的是为了防止每次调用还要定义一遍这个数组会有一定的消耗。
此外对于判断闰年和平年这里,还要注意先把month==2写在最前面,因为只有2月是需要判断的
重载+=运算符:
Date& Date::operator+=(int day) { if (day < 0) { return *this -= (-day); } _day += day; while (_day > GetMonthDay(_year, _month)) { //月进位 _day -= GetMonthDay(_year, _month); _month++; if (_month == 13) { _year++; _month = 1; } } return *this; }
注意:对于if语句为什么复用了-=运算符是因为加一个负的天数,是算多少天以前的日期,所以当天数为负的时候,可以复用-=。
重载+运算符:
+运算符和+=运算符本质是一样的,所以不需要再写一遍,只需要+复用+=运算符就可以了,但是+=运算后等价于a=a+b,a也是会改变的,而+运算符运算后a是不会改变的
Date Date::operator+(int day) { Date tmp(*this); 复用+= tmp += day; return tmp; }
注意:要计算n+m的最终结果,n和m这两个数值计算后是不会改变的,所以一个日期加天数,原日期是不会改变的,而原日期也就是this指针指向的内容,所以也就是不能修改this指针指向的内容,对此要先利用拷贝构造函数创建一个和*this一模一样的对象tmp,在tmp的基础上去加天数。此外tmp是一个临时变量,出了作用域会销毁,所以不能引用返回
为什么不用+=复用+:
注意:上面的复用,只能存在一个,不能同时都去复用,同时存在会出现错误。
重载-=运算符:
对于上述是+=复用+还是+复用+=好给出原因,正因如此,-=和-也是一样的原理,所以先写-=再用-复用-=
Date& Date::operator-=(int day) { 这里和上述是一个意思 if (day < 0) { return *this += (-day); } _day -= day; while (_day <= 0) { --_month; if (_month == 0) { --_year; _month = 12; } _day += GetMonthDay(_year, _month); } return *this; }
重载-运算符:
Date Date::operator-(int day) { Date tmp(*this); tmp -= day; return tmp; }
重载日期-日期:
int Date::operator-(const Date& d) { Date max = *this; Date min = d; int flag = 1; if (max < min) { max = d; min = *this; flag = -1; } int n = 0; while (min != max) { ++min; ++n; } return n * flag; }
计算的结果是两个日期之间的天数,所以返回值是int,要想知道两个日期之间相隔的天数,可以设置一个计数器n,让小日期一直加到大日期,就可以知道两个日期之间相隔的天数。
重载前置++和后置++运算符:
++d3 前置++,返回++之后的
++d3 —> d3.operator++()
———————————————————————————————————————————
d3++ 后置++,返回++之前的
++d3 —> d3.operator++(0) 这里加参数是为了区分前置++和后置++
加一个int参数,进行占位,跟前置++构成函数重载进行区分,本质后置++调用,编译器进行特殊处理
自定义类型用前置++比较好,后置++需要拷贝代价比较大 Date& Date:: operator++() { *this += 1; 复用了+= return *this; } —————————————————————————————————————————————————————————————————————————————————— Date Date:: operator++(int) { Date tmp(*this);拷贝构造 *this += 1; return tmp; } int main() { Date d3(2023, 7, 27); 下面两者等价不过是前者显式调用,对于后置++或者后置--显式调用需要传个参数不过并无卵用 Date ret1 = d3.operator++(0);//Date ret1=d3--; ret1.Print(); d3.Print(); Date ret2 = ++d3; ret2.Print(); d3.Print(); return 0; }
重载前置--和后置--运算符:
Date& Date:: operator--() { *this -= 1; 复用了-= return *this; } ———————————————————————————————————————————————————————————————————————————————— Date Date:: operator--(int) { Date tmp(*this); *this -= 1; return tmp; } int main() { Date d3(2023, 7, 27); 下面两者等价不过是前者显式调用,对于后置++或者后置--显式调用需要传个参数不过并无卵用 Date ret1 = d3.operator--(0);//Date ret1=d3--; ret1.Print(); d3.Print(); Date ret2 = --d3; ret2.Print(); d3.Print(); return 0; }
>>和<<运算符:
对于内置类型可以直接使用<<、>>,编译器可以根据数据的类型进行打印,本质上是因为库中进行运算符重载。但是对于自定义类型,编译器是不知道怎样打印的,所以要想使用<<打印(自定义类型)日期类是要对运算符<<进行重载
我们在使用C++进行输入输出的时候,会用到cin和cout,它们俩本质上都是对象,cin是istream类实例化的对象,cout是ostream类实例化的对象。
而<<、>>不用像C语言的printf和scanf那样,char对应%c,int对应%d,float对应%f,是因为运算符重载本质上是函数,对这些不同的内置类型,分别进行了封装,在运算符重载的基础上又实现了函数重载,所以<<、>>支持自动识别类型。
重载<<运算符:
Date.h void operator<<(ostream& cout);//放在类里面的 Date.cpp void Date::operator<<(ostream& cout)//--->operator(Date* this,ostream& cout) { cout << _year << "-" << _month << "-" << _day << endl; } Test.cpp cout << d1; d1 << cout;//虽然可以运行,但是不符合使用习惯和价值
当在类中重载<<运算符会发现用cout<<d1打印是会发生报错的,这是因为对于一个双目运算符的重载,它的左操作数(cout)会传递给运算符重载函数的第一个形参(this),右操作数(d1)会传递给运算符重载函数的第二个形参(cout),对于类的成员函数,它的第一个形参是默认的this指针,该指针是日期类类型的指针,和cout的类型不匹(—>operator(Date* this,ostream& cout))。为了解决上述问题,可以写成d1<<cout,此时就相当于d1.operator(cout),会把d1的地址传给this指针,形参再用一个ostream类型的对象来接收cout,虽然可以运行,但是不符合使用习惯和价值。
🌟最好的解决方式:把<<重载成全局函数
把<<重载成全局函数,就不会有默认的this指针,同时,还可以根据需要设置形参的顺序,void operator<<(ostream& out,const Date& d),出了作用域cout还在,所以可以用引用返回。
Date.h void operator<<(ostream& cout,const Date& d);//全局的 Date.cpp void operator<<(ostream& cout, const Date& d) { cout << d._year << "-" << d._month << "-" << d._day << endl; } Test.cpp cout << d1;
把<<重载成全局函数是解决了参数问题,但是默认在该全局函数中是访问不到日期类中的私有成员变量,为了解决这个问题,可以把该运算符重载函数设置成友元函数
🌟友元声明:在日期类里面第一行加上一条下述声明,此时在该全局函数体就可以使用日期类中的私有成员变量。
friend void operator<<(ostream& cout, const Date& d); 此声明不受类中访问限定符的限制
🌟连续的<<
Date.h ostream& operator<<(ostream& cout,const Date& d); Date.cpp ostream& operator<<(ostream& cout, const Date& d) { cout << d._year << "-" << d._month << "-" << d._day << endl; return cout; } Test.cpp cout << d1 << d2 << endl;
对于上述cout<<d1,是一个函数调用,返回类型是void,但是对于连续的<<,cout<<d1<<d2<<endl;cout先是作为左操作数打印d1,左向右执行cout<<d1;返回void,后面的d2就会报错,所以要写成ostream& operator<<(ostream& cout, const Date& d),返回一个ostream类型的cout,接着cout作为左操作数再次函数调用打印d2
当然这里改了友元声明也要改
friend ostream& operator<<(ostream& cout, const Date& d);
重载>>运算符:
希望通过流插入往d里面写入数据,所以不能加const修饰。istream& operator>>(istream& cin, Date& d);
Date.h istream& operator>>(istream& cin, Date& d); Date.cpp istream& operator>>(istream& cin, Date& d) { cin >> d._year >> d._month >> d._day; return cin; } Test.cpp cin >> d3;
🌟友元声明
friend istream& operator>>(istream& cin, Date& d);
C++中的流插入和流提取可以完美的支持自定义类型的输入输出
五、const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数( void Print() const ),实际修饰该成员函数隐含的this指针( void Print(const Date* this) ),表明在该成员函数中不能对类的任何成员进行修改。这样,不仅普通对象可以调用该成员函数(权限的缩小),const对象也能调用该成员函数(权限的平移)。也防止了权限的放大。
const权限问题:
#include<iostream> using namespace std; class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << "Print()" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } void Print() const // ---> void Print(const Date* this) { cout << "Print()const" << endl; cout << "year:" << _year << endl; cout << "month:" << _month << endl; cout << "day:" << _day << endl << endl; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; void Test() { Date d1(2022, 1, 13); d1.Print(); const Date d2(2022, 1, 13); d2.Print(); } int main() { Test(); return 0; }
🌟权限不可以放大
总结:
对于关系运算符可以考虑加上const修饰,因为并不会改变对象本身,但是并不是所有的成员函数都要加const修饰,要修改对象成员变量的函数,例如:重载的+=、-=等,是不能加const修饰的,因为会修改成员本身,而成员函数中如果没有修改对象的成员变量,可以考虑加上const修饰,这样不仅普通对象可以调用该成员函数(权限的缩小),const对象也能调用该成员函数(权限的平移)
六、取地址及const取地址操作符重载
#include<iostream> #include"Date.h" using namespace std; Date* Date::operator&() { cout << "Date* operator&()" << endl; return this; } const Date* Date::operator&()const { cout << "const Date* operator&()const" << endl; return this; return nullptr; 不想被取到有效地址可以这样写 } int main() { Date d1(2023,10,1); const Date d2(2023,10,1); cout << &d1 << endl; cout << endl; cout << &d2 << endl; return 0; }
🌟总结:这两个取地址运算符重载函数,又构成函数重载。这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容