【C++类和对象中:解锁面向对象编程的奇妙世界】(三):https://developer.aliyun.com/article/1425465
我们来实现一下-=操作符重载和-操作符重载,这里仍然是让-操作符重载复用-=操作符重载
Date& operator-= (int day) { //如果传入的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 operator-(int day) { Date tmp = *this; tmp -= day; return tmp; }
结果验证:
5.3 前置++和后置++重载
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // 前置++:返回+1之后的结果 // 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率 Date& operator++() { *this += 1; return *this; } // 后置++: // 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载 // C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器\ 自动传递 // 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存\ 一份,然后给this + 1 // 而temp是临时对象,因此只能以值的方式返回,不能返回引用 Date operator++(int) { Date temp(*this); *this += 1; return temp; } Date& operator--() { *this -= 1; return *this; } Date operator--(int) { Date temp(*this); *this -= 1; return temp; } private: int _year; int _month; int _day; }; int main() { Date d; Date d1(2023, 10, 27); d = d1++; // d: 2023,10,27 d1:2023,10,28 d = ++d1; // d: 2023,10,29 d1:2023,10,29 return 0; }
5.4 日期相减重载
// d1 - d2 Date operator-(const Date& d) { //假设左大右小 int flag = 1; Date max = *this; Date min = d; //假设错了,左小右大,此时相减就是负数 if (*this < d) { max = d; min = *this; flag = -1; } int n = 0; while (min != max) { ++min; ++n; } return n * flag; }
我们再来回顾一下我们上面的Print函数,每次该成员函数都要我们自己去实现,比较麻烦,我们可以通过运算符重载去实现打印,由于此时是自定义类型,所以可以通过重载流插入运算符实现。
cout是ostream类型的对象,cin是istream类型的对象
内置类型可以支持流插入操作,是因为库里面已经实现过了。
#include <iostream> using namespace std; class Date { public: // 全缺省的构造函数 Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void operator << (ostream& out)//cout是ostream的对象 { out << _year << "年" << _month << "月" << _day << "日"; } private: int _year; int _month; int _day; }; int main() { Date d1(2023, 11, 5); cout << d1;//报错,why?? d1 << cout;//运行成功!! return 0; }
运行结果:
双操作数的运算符,第一个参数是左操作数,第二个操作数是右操作数,作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this,所以对于第二种写法,第一个参数是d1,第二个参数是cout,可以转化为d1.operator<<(&d1,cout),对于第一种写法参数就不匹配,所以报错。虽然第二种写法是正确的,但是看着不习惯,要想第一种此恶法正确,我们必须第一个参数设置为cout。因此对与该函数我们可以写成全局函数。
#include <iostream> using namespace std; class Date { public: // 全缺省的构造函数 Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //private://此时必须注释,否则全局函数访问不到该变量 int _year; int _month; int _day; }; void operator << (ostream& out,Date &d)//cout是ostream的对象 { out << d._year << "年" << d._month << "月" << d._day << "日"; } int main() { Date d1(2023, 11, 5); cout << d1; //d1 << cout; return 0; }
此时就转化成了operator<<(cout,d1),虽然上面我们确实通过将该函数放到全局变量中实现了我们的流插入操作符函数,但是使用该函数的时候我们必须要将类的成员变量设置公开,那么这样不就与封装性相悖吗?我们可以通过友元来解决。
#include <iostream> using namespace std; class Date { public: // 全缺省的构造函数 Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //友元函数 friend void operator << (ostream& out,Date &d);//cout是ostream的对象 private: int _year; int _month; int _day; }; void operator << (ostream& out,Date &d)//cout是ostream的对象 { out << d._year << "年" << d._month << "月" << d._day << "日"; } int main() { Date d1(2023, 11, 5); cout << d1; //d1 << cout; return 0; }
上面使用了友元函数,这里介绍一下,友元函数就是相当你有一个私人游泳池(相当于类的私有成员变量),然后你的非常要好朋友可以直接去你的私人游泳池游泳(访问私有变量),此时你不介意。我们的流插入操作符还可以支持连续使用,想要连续使用就必须设置一个返回值,所以我们要修改一下上面的代码
#include <iostream> using namespace std; class Date { public: // 全缺省的构造函数 Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //友元函数 friend ostream& operator << (ostream& out,const Date &d);//cout是ostream的对象 private: int _year; int _month; int _day; }; //流插入的内容d不会改变,可以加上const修饰 ostream& operator << (ostream& out,const Date &d)//cout是ostream的对象 { out << d._year << "年" << d._month << "月" << d._day << "日"; return out; } int main() { Date d1(2023, 11, 5); Date d2; //赋值运算符顺序:从右往左 //流插入运算符顺序:从左往右 //由运算符的结合性决定 //这里的返回值必须是cout cout << d1 << d2; return 0; }
我们再来实现一下流提取运算符重载
#include <iostream> using namespace std; class Date { public: // 全缺省的构造函数 Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //友元函数 friend ostream& operator << (ostream& out,const Date &d);//cout是ostream的对象 friend istream& operator >> (istream& in,Date &d);//cin是istream的对象 private: int _year; int _month; int _day; }; //流插入的内容d不会改变,可以加上const修饰 ostream& operator << (ostream& out,const Date &d)//cout是ostream的对象 { out << d._year << "年" << d._month << "月" << d._day << "日"; return out; } //流插入的内容d会改变,不可以加上const修饰 istream& operator >> (istream& in,Date &d)//cin是istream的对象 { in >> d._year >> d._month >> d._day; return in; } int main() { Date d1(2023, 11, 5); Date d2; //赋值运算符顺序:从右往左 //流插入运算符顺序:从左往右 //由运算符的结合性决定 //这里的返回值必须是cout cin >> d1 >> d2; cout << d1 << d2; return 0; }
运行结果:
总结:其他的运算符一般实现成成员函数,流插入和流提取操作符必须实现到全局中,这样才能让流对象作第一个参数。流本质是为了解决自定义类型输入和输出问题,C语言的printf函数只能指定内置类型,同时打印输出的时候还要指定输出格式,C语言的printf函数和scanf函数无法解决自定义类型的输入输出问题。C++通过面向对象和运算符重载来解决。
6.日期类的实现
class Date { public: // 获取某年某月的天数 int GetMonthDay(int year, int month) { static int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; int day = days[month]; if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { day += 1; } return day; } // 全缺省的构造函数 Date(int year = 1900, int month = 1, int day = 1); // 拷贝构造函数 // d2(d1) Date(const Date& d); // 赋值运算符重载 // d2 = d3 -> d2.operator=(&d2, d3) Date& operator=(const Date& d); // 析构函数 ~Date(); // 日期+=天数 Date& operator+=(int day); // 日期+天数 Date operator+(int day); // 日期-天数 Date operator-(int day); // 日期-=天数 Date& operator-=(int day); // 前置++ Date& operator++(); // 后置++ Date operator++(int); // 后置-- Date operator--(int); // 前置-- Date& operator--(); // >运算符重载 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); // 日期-日期 返回天数 int operator-(const Date& d); private: int _year; int _month; int _day; };
7.const成员
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
我们来看看下面的代码
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; } 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限制时,这里报错了为什么呢?
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this,这个隐藏的this类型的Date* cosnt this,而我们传入的类型时const Date* this。这里就会存在权限放大的问题,我们需要在隐藏的this类型的前面加上cosnt,但是this类型是隐藏的,我们应该怎么加呢?C++规定在函数之后写上cosnt就可以对隐藏的this类型的前面加上cosnt。
class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() const { cout << "Print()" << 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。成员函数定义的原则:1、能定义成cosnt的成员函数都应该定义成cosnt,这样const对象(权限平移)和非const对象(权限缩小)都可以调用。2、要修改成员变量的成员函数,不能定义成cosnt。3、流插入和流提取不能加上cosnt,它们不是成员函数,没有this指针,是全局函数。
请思考下面的几个问题:
1. const对象可以调用非const成员函数吗?不可以,权限不能放大
2. 非const对象可以调用const成员函数吗?可以,权限可以缩小
3. const成员函数内可以调用其它的非const成员函数吗?
4. 非const成员函数内可以调用其它的const成员函数吗?
8.取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date* operator&() { cout << "Date * operator&()" << endl; return this; } const Date* operator&()const { cout << "const Date * operator&()const" << endl; return this; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1; const Date d2; cout << &d1 << endl; cout << &d2 << endl; return 0; }
运行结果:
为什么这里要写两个取地址操作符重载呢?因为普通对象返回Date*,而const对象返回cosnt Date*,这两个是不同的,参数也是不同的,所以能构成重载。如果我们注释第一个非const成员函数。
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //Date* operator&() //{ // cout << "Date * operator&()" << endl; // return this; //} const Date* operator&()const { cout << "const Date * operator&()const" << endl; return this; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1; const Date d2; cout << &d1 << endl; cout << &d2 << endl; return 0; }
运行结果:
此时我们发现就会调用非cosnt成员函数,此时发生了参数和返回值权限的缩小。这个就和我们吃饭一样,有好吃的有自己相吃的就去吃,没有的就将就一下。
class Date { public: Date(int year = 1900, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //Date* operator&() //{ // cout << "Date * operator&()" << endl; // return this; //} /*const Date* operator&()const { cout << "const Date * operator&()const" << endl; return this; }*/ private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1; const Date d2; cout << &d1 << endl; cout << &d2 << endl; return 0; }
运行结果:
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需 要重载,比如想让别人获取到指定的内容!