一.为什么C++会有运算符重载这个语法呢?
1.需求说明
有的时候对于某些类来说,我们会有一些需求让我们去实现一些函数,能够便捷快速地对该类的若干成员变量进行数据操作
以日期类为例,有些时候我们想要去判断两个日期谁大谁小,是否相等,计算两个日期之间相差多少天,计算某一个日期加上几天后的日期是多少等等等等的需求
2.实现
有需求,就会有一大堆解决方案.
我们这里以比较日期大小和Date类为例:
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; };
其中不太好的解决方案是:
1.不规范的解决方案
1.代码实现
bool Greater(const Date& d1, const Date& d2) { if (d1._year > d2._year) { return true; } else if (d1._year == d2._year && d1._month > d2._month) { return true; } else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day) { return true; } return false; }
这里这么多报错呢?
因为我们的日期类的成员变量的访问权限是private,私有
在类外访问不到
怎么办呢?
这个我们先暂时抛开不谈,我们下面会回过头来解释解决方法的.
这里我们先把成员变量设置为公有
实现的挺好的啊,怎么啦?
2.缺陷
有一点不太好的地方,例如:
这个函数的名字叫做Greater(起的挺好)
但是:万一有人这么去定义名字呢?
bool dayu(const Date& d1, const Date& d2) {//具体实现} bool compare1(const Date& d1, const Date& d2) {//具体实现} 如果是比较是否相等的函数 bool xiangdeng(const Date& d1, const Date& d2) {//具体实现} bool compare2(const Date& d1, const Date& d2) {//具体实现}
这是不是不太好啊,如果跟一些外国人一起合作做一些项目开发什么的话
他们不一定能看懂啊:dayu,xiangdeng
而且compare1和compare2对我们中国人也不是很友好,没有人规定1就是大于,2就是小于啊…
所以为了便于代码的跨国际通用性,C++创始人就引入了运算符重载这一语法
3.具体的解决方案:运算符重载
下面我们让开始一起来探索运算符重载的奥秘吧
具体的解决代码在我们学完运算符重载之后我们会给出来的
二.运算符重载的语法形式
受到了数学中运算符的启发,C++创始人就想:
在C语言中运算符只能操作一些内置的数据类型,那么可不可以让它们也能操作一下自定义类型呢?
既然我们都已经决定要去规范一下大家的代码了,那么可不可以把数学符号引入到自定义类型的比较中来呢?
1.语法形式
bool operator>(const Date& d1, const Date& d2) { if (d1._year > d2._year) { return true; } else if (d1._year == d2._year && d1._month > d2._month) { return true; } else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day) { return true; } return false; }
具体调用:
bool ret = operator>(d1, d2);
这个函数比起Greater来说只是改了一下名字
那么你可能会觉得,不过如此嘛
但是,惊喜在这里呢~:
具体调用:
bool ret = d1 > d2;
大于运算符真的可以对自定义类型进行比较了!
为什么呢?
C++创始人设计运算符重载时想:我都已经设计好运算符重载这一语法了,可是调用的话还要加个operator,还是有点麻烦啊,要不然就让编译器去把operator优化一下吧
让我们能够真的只用一个>就能比较两个自定义类型
2.private私有成员的解决方案
但是:我们之前提到过,这个类中的私有成员,类外不能访问,那么怎么办呢?
1.封装出get函数,能够在类外读取对应成员变量的数值
这里为什么会报错呢?因为我们在这里加了const修饰,导致d1和d2不能去调用对应的成员函数,因为编译器怕我们在成员函数中修改成员变量,违背了const的修饰
那我们就去掉const
成功运行
但是也太麻烦了吧,我还需要再去单独设计三个函数来保证类外可以读取类内部的数据
那如果我都不想让类外部读取到我类内部的数据呢?
2.把运算符重载放到类内部
不就这么简单吗,至于这么大费周章吗,我还以为能有什么高明的手段呢.
你错了:
然后我们把这个运算符重载移到类内部,
结果发现:这里竟然报错了
为什么会报出参数太多的错误来呢?
因为类的成员函数的参数列表中有一个隐含的this指针!!!
而且运算符重载有一个规定:
操作的数据个数要与参数个数匹配上
这里我们操作的数据个数是2个,可是参数个数有3个,因此报出了参数太多的错误
怎么修改呢?
把一个参数改为this指针
也就是这样:
bool operator>(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; } return false; }
调用方法: 1.d1>d2; 2.d1.operator>(d2) 本质是:operator(&d1,d2);this指针指向d1
这就是>的运算符重载的版本
3.其他比较运算符重载的代码
下面一起给出==,!=,<=,>=,<的重载版本了 并且对构造函数传入的日期进行了规范化检查 其实只要我们写出>和==或者<和== 就可以借助于逻辑与或者逻辑或,逻辑取反操作符来进行复用
其中*this就是当前调用这个运算符函数的对象
class Date { public: Date(int year = 1, int month = 1, int day = 1) { //对传入的日期进行检查 if ((year < 0) || (month < 1 || month>12) || (day<1 || day>GetMonthDay(year, month))) { //assert(false);//粗暴一点的方法 Print();//本质是:this->Print(); cout << "输入的日期为非法日期" << endl; exit(-1); } _year = year; _month = month; _day = day; } void Print() { cout << _year << " " << _month << " " << _day << endl; } bool operator>(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; } return false; } bool operator<=(Date& d) { return !(*this > d); } bool operator>=(Date& d) { return *this == d || *this > d; } bool operator<(Date& d) { return !(*this >= d); } bool operator==(Date& d) { return _year == d._year && _month == d._month && _day == d._day; } bool operator!= (Date& d) { return !(*this == d); } private: int _year; int _month; int _day; };
当我们引出了运算符重载这个知识点并且实现了>,==,等等运算符的重载之后
三.赋值运算符重载
接上我们类和对象中这个大模块的知识,下面我们来介绍赋值运算符重载这个类的默认成员函数
1.赋值运算符重载为什么能够作为默认成员函数之一呢?
为什么运算符重载中只有赋值运算符会成为类的6大默认成员函数之一呢?
我的理解是:
2.赋值运算符的返回值类型
那么赋值运算符重载的返回值类型是什么呢?
这就要看一下=这个运算符本身的返回值类型了
=这个运算符本身就可以这样连着写,因此b=c这个表达式返回的就是b本身
因此赋值运算符重载的返回值类型是该类对象的引用
3.赋值运算符重载的具体实现
具体代码:
Date5& operator=(Date5& d) { //对传入的日期进行检查 if ((d._year < 0) || (d._month < 1 || d._month>12) || (d._day<1 || d._day>GetMonthDay(d._year, d._month))) { //assert(false);//粗暴一点的方法 cout << "拷贝对象的日期为非法日期: "; d.Print(); cout << "无法进行拷贝" << endl; exit(-1); } _year = d._year; _month = d._month; _day = d._day; return *this; }
我们来看一下拷贝构造函数的代码
Date5(Date5& d1) { //对传入的日期进行检查 if ((d1._year < 0) || (d1._month < 1 || d1._month>12) || (d1._day<1 || d1._day>GetMonthDay(d1._year, d1._month))) { //assert(false);//粗暴一点的方法 cout << "拷贝对象的日期为非法日期: "; d1.Print(); cout << "无法进行拷贝" << endl; exit(-1); } _year = d1._year; _month = d1._month; _day = d1._day; }
4.Stack类的赋值运算符重载实现
Stack& operator=(const Stack& st) { if (_a != nullptr) { free(_a);//先释放原有空间 _a = nullptr; } _a = (int*)malloc(sizeof(int) * st._capacity); if (nullptr == _a) { perror("malloc fail"); exit(-1); } _capacity = st._capacity; memcpy(_a, st._a, sizeof(int) * st._top); _top = st._top; return *this; }
5.赋值运算符重载跟拷贝构造函数的区别与联系
关于拷贝构造函数,大家可以看我的另一篇博客:
里面对拷贝构造函数的来龙去脉进行了详细的讲解
6.注意
下面给大家调试一下,我们按F11进入函数
证明完毕
尽管:
Date d2 = d;
看起来像是调用运算符重载函数
但是d2是一个尚未被建立的对象,当前正在用d的数据来对d2进行构造,也就是拷贝构造
这也就印证了这句话:
拷贝构造函数:是利用一个已经存在的对象去拷贝一份还未被创建的对象
而赋值运算符重载:是两个已经存在的对象之间进行拷贝赋值
四.+和+=的运算符重载
1.+=的运算符重载
1.+=有没有返回值呢?
对于一个内置类型来说,我们在这里以两个int类型为例,来探讨一下两个int类型进行+=计算有没有返回值
可见,是有返回值的
这里我们要实现的是让一个日期+=天数
2.+=的实现
因为每个月份的天数都不同,而且二月在闰年是29天,在平年是28天
所以我们有必要实现一个函数来获取当前月份有多少天
int GetMonthDay(int year, int month) { int MonthArray[13] = { 0,31,28,31,30,31,30,31,30,31,31,30,31 }; if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) { MonthArray[month]++; } return MonthArray[month]; } Date operator+=(int day) { _day += day; while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); _month++; if (_month == 13) { _year++; _month = 1; } } //从这里我们就可以看出*this在某些情况下是需要在成员函数内部显式使用的 return *this;//*this就是调用+=这个运算符的变量 }
从这里我们就可以看出*this在某些情况下是需要在成员函数内部显式使用的
可不可以再优化一些呢?
其实我们这个函数是可以传引用返回的,因为:
Date d1(2023, 10, 23); d1 += 70; 返回的是d1,d1本身并不会随着+=这个运算符函数的销毁而销毁的,因此可以传引用返回 这也是引用作为返回值的一大应用
Date& operator+=(int day) { _day += day; while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); _month++; if (_month == 13) { _year++; _month = 1; } } return *this;//*this就是调用+=这个运算符的变量,而且这个*this出了这个+=函数之后依然存在,所以可以返回引用 }
2.+的运算符重载
Date operator+(int day) { Date d2(*this);//这里调用拷贝构造函数 d2._day += day; while (d2._day > GetMonthDay(d2._year, d2._month)) { d2._day -= GetMonthDay(d2._year, d2._month); d2._month++; if (d2._month == 13) { d2._year++; d2._month = 1; } } return d2; }
那么问题来了,这个+的运算符重载可以传引用返回吗?
当然不可以
为什么呢?
d2是+这个运算符函数中的局部变量,随着+这个函数栈帧的销毁,d2也会随之销毁
因此传只能传值返回,而且返回的不是d2,而是d2的一份临时拷贝
其实这里也不需要这么麻烦,我们是可以复用+=这个运算符的
Date operator+(int day) { Date d2(*this);//这里调用拷贝构造函数 d2 += day; return d2; }
3.可不可以让+=复用+呢?
当然可以啦,我们刚刚介绍的赋值运算符重载函数就能派上用场了
Date& operator+=(int day) { *this = *this + day; return *this; }