有的时候对于某些类来说,我们会有一些需求让我们去实现一些函数,能够便捷快速地对该类的若干成员变量进行数据操作
以日期类为例,有些时候我们想要去判断两个日期谁大谁小,是否相等,计算两个日期之间相差多少天,计算某一个日期加上几天后的日期是多少等等等等的需求
有需求,就会有一大堆解决方案.
我们这里以比较日期大小和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;
};
其中不太好的解决方案是:
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,私有
在类外访问不到
怎么办呢?
这个我们先暂时抛开不谈,我们下面会回过头来解释解决方法的.
这里我们先把成员变量设置为公有
实现的挺好的啊,怎么啦?
有一点不太好的地方,例如:
这个函数的名字叫做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++创始人就引入了运算符重载这一语法
下面我们让开始一起来探索运算符重载的奥秘吧
具体的解决代码在我们学完运算符重载之后我们会给出来的
受到了数学中运算符的启发,C++创始人就想:
在C语言中运算符只能操作一些内置的数据类型,那么可不可以让它们也能操作一下自定义类型呢?
既然我们都已经决定要去规范一下大家的代码了,那么可不可以把数学符号引入到自定义类型的比较中来呢?
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优化一下吧
让我们能够真的只用一个>就能比较两个自定义类型
但是:我们之前提到过,这个类中的私有成员,类外不能访问,那么怎么办呢?
这里为什么会报错呢?因为我们在这里加了const修饰,导致d1和d2不能去调用对应的成员函数,因为编译器怕我们在成员函数中修改成员变量,违背了const的修饰
那我们就去掉const
成功运行
但是也太麻烦了吧,我还需要再去单独设计三个函数来保证类外可以读取类内部的数据
那如果我都不想让类外部读取到我类内部的数据呢?
不就这么简单吗,至于这么大费周章吗,我还以为能有什么高明的手段呢.
你错了:
然后我们把这个运算符重载移到类内部,
结果发现:这里竟然报错了
为什么会报出参数太多的错误来呢?
因为类的成员函数的参数列表中有一个隐含的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
这就是>的运算符重载的版本
下面一起给出==,!=,<=,>=,<的重载版本了
并且对构造函数传入的日期进行了规范化检查
其实只要我们写出>和==或者<和==
就可以借助于逻辑与或者逻辑或,逻辑取反操作符来进行复用
其中*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;
};
当我们引出了运算符重载这个知识点并且实现了>,==,等等运算符的重载之后
接上我们类和对象中这个大模块的知识,下面我们来介绍赋值运算符重载这个类的默认成员函数
为什么运算符重载中只有赋值运算符会成为类的6大默认成员函数之一呢?
那么赋值运算符重载的返回值类型是什么呢?
这就要看一下=这个运算符本身的返回值类型了
=这个运算符本身就可以这样连着写,因此b=c这个表达式返回的就是b本身
因此赋值运算符重载的返回值类型是该类对象的引用
具体代码:
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;
}
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;
}
关于拷贝构造函数,大家可以看我的另一篇博客:
C++类和对象中(构造函数,析构函数,拷贝构造函数)详解
里面对拷贝构造函数的来龙去脉进行了详细的讲解
下面给大家调试一下,我们按F11进入函数
证明完毕
尽管:
Date d2 = d;
看起来像是调用运算符重载函数
但是d2是一个尚未被建立的对象,当前正在用d的数据来对d2进行构造,也就是拷贝构造
这也就印证了这句话:
拷贝构造函数:是利用一个已经存在的对象去拷贝一份还未被创建的对象
而赋值运算符重载:是两个已经存在的对象之间进行拷贝赋值
对于一个内置类型来说,我们在这里以两个int类型为例,来探讨一下两个int类型进行+=计算有没有返回值
可见,是有返回值的
这里我们要实现的是让一个日期+=天数
因为每个月份的天数都不同,而且二月在闰年是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出了这个+=函数之后依然存在,所以可以返回引用
}
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;
}
当然可以啦,我们刚刚介绍的赋值运算符重载函数就能派上用场了
Date& operator+=(int day)
{
*this = *this + day;
return *this;
}
对于+复用+=来说
+=:拷贝了0次
+:拷贝了2次:分别是:
对于+=复用+来说
+拷贝了2次:分别是:
+=拷贝了3次:分别是:
因此使用+复用+=更好
同理,使用-复用-=更好
在+=复用+的版本中
但是当我们写好了这个复用函数后会发现编译器报错
这是为什么呢?
是因为我们所写的赋值运算符重载函数
Date& operator=(Date& d);
这个参数列表中没有加上const修饰
而*this=*this+day;
这个表达式的右值返回的是一个临时变量,而临时变量具有常性
也就可以认为这个临时变量具有const修饰
然后继续调用赋值运算符重载函数,可是赋值运算符重载函数的参数并没有const修饰,因此出现了权限的放大问题,进而报错
同理,如果我们这样调用拷贝构造函数也是如此
Date d((*this) + day);
我们可以直接复用+=运算符
//前置++:拷贝0次
Date& operator++()
{
*this += 1;
return *this;
}
这里注意:
因为+=已经考虑了不同月份的具体天数不同,因此我们这里就不需要去管不同月份天数的影响了
C++创始人规定
前置++这样去定义:
Date& operator++()
后置++这样去定义:
Date operator++(int)
这里就印证了我们之前在学习缺省参数的时候学习到的一点:
编译器允许函数的参数列表中的参数只写类型,不写名称
这个参数设计的目的就是为了区分前置++和后置++
那么为什么要设计为int呢?
没有为什么,C++创始人就是这么规定的
注意这里的红字
就是先
_day-=day;
然后向月份借天数,注意:向前一个月份借天数
直到_day>0为止
注意:
如果有人就是想要-=一个负数
那就相当于+=这个负数的相反数
因此我们在这里加了一个if先去判断
Date& operator-=(int day)
{
//减一个负数==加上这个负数的相反数
if (day < 0)
{
return (*this) += (-day);
}
_day -= day;
while (_day <= 0)
{
//因为接下来要向前一个月份借天数,所以先让月份--
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
同理:对于+=,我们也可以去改进一下
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)
{
_month = 1;
_year++;
}
}
return *this;
}
Date operator-(int day)
{
Date d(*this);
d -= day;
return d;
}
还有最后一个需求
我们想要去计算两个日期之间相差多少天
因为每个月的天数都不相同,而且还有闰年的干扰
所以只能这么去计算相差多少天
int operator-(Date& d)
{
Date max = *this;
Date min = d;
int flag = 1;//假设:*this大于d,左大于右 左-右
//假设错误
if (min > max)
{
max = d;
min = *this;
flag = -1;//右大于左
}
int day = 0;
while (min < max)
{
//++min;//++min是代码实现是调用+=的,那我直接调用+=不好吗,更快
min += 1;
day++;
}
return flag * day;
}
我们之前提到过C++创始人对于C语言中的左移运算符<<和右移运算符>>
又分别添加了一种语法
规定<<不仅可以表示左移运算符,也可以表示流插入运算符
/>>不仅可以表示右移运算符,也可以表示流提取运算符
对于内置类型:
我们可以这样去打印:
说明流插入是可以自动识别类型的
那么我们可不可以这样做呢?
当然是做不了的
为什么呢?
因为当我们没有重载流插入运算符时,编译器是无法得知我们想要怎样打印这个自定义类型的
因此流插入运算符是无法识别自定义类型的
那么为什么流插入运算符能够自动识别内置类型呢?
其实是因为C++库中已经实现好了这些自定义类型的流插入运算符重载函数
而cout和cin的类型分别是:
cout的类型是:ostream
cin的类型是:istream
我们可以这么理解:
平常我们使用cout来打印数据时
例如:
cout<<100;
本质是调用了<<的一个重载版本:
我们可以理解为:cout是这样打印的
在这个运算符重载的类当中
cout被隐含的this指针传过去,而另一个参数就是我们要打印的这个100
这个<<的重载函数是这样的:
ostream& operator<<(int val);
cout.operator<<(100);
于是我们就可以写出<<的重载版本
void Date::operator<<(ostream& out)
{
out << _year << " " << _month << " " << _day << endl;
}
这里我们用out作为参数,跟cout的名字区分一下
其实当我们调用cout<
然后我们去打印,但是发现了一个"奇怪"的现象
使用cout<
使用d1<
void operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
}
此时就可以正常运行了
因此我们只需要这样做
就可以正常运行了
下面的问题是<<流插入运算符是支持连续进行的啊
那么我们可不可以也这样呢?
报错
为什么呢?
因为我们刚才实现的是
上面的分析过程就有点类似于物理中的整体+隔离法进行受力分析了
下面我们来实现一下:
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;
}
类似于流插入<<,流提取也可以按照同样的思路去重载
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
有些情况下,我们需要定义出const修饰的类型的对象
而这时,因为我们的成员函数中的this指针默认是没有const去修饰的
那么这种情况下就会出现这种错误
怎么办呢?
我们无法显式定义this指针,那么就无法直接在this指针的类型前面加上const修饰
于是C++创始人规定:
我们可以这样去给一个成员函数的this指针加上const修饰
void Print() const
{
cout << _year << " " << _month << " " << _day << endl;
}
下面就是一个典型的错误示范:
而当我们给>运算符函数加上const修饰之后
正常运行
这是最后两个类的默认成员函数
既然是类的默认成员函数,那么就说明:如果我们没有定义取地址运算符重载函数,那么编译器会生成默认的取地址运算符重载函数
它们的具体实现如下:
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
这两个运算符一般不需要重载,使用编译器生成的即可,只有特殊情况,才需要重载,比如不想让别人得到正确的地址
Date* operator&()
{
return nullptr;
//或者直接返回一个错误地址
return (Date*)0x11223344;
}
我们将Date类的声明跟定义分离
结果发现:
也就是:缺省参数的缺省值不能同时在声明和定义中都有
规定要在声明中给出,定义中不要给缺省值
这样就可以了
#pragma once
#include
using namespace std;
#include
class Date
{
public:
void Print() const;
Date(int year = 1, int month = 1, int day = 1);
Date(const Date& d1);
Date& operator=(const Date& d);
bool operator==(const Date& d) const;
bool operator!=(const Date& d) const;
bool operator>(const Date& d) const;
bool operator<=(const Date& d) const;
bool operator>=(const Date& d) const;
bool operator<(const Date& d) const;
//返回year年month月有多少天
int GetMonthDay(int year, int month);
//上面这个+复用+=更好
//拷贝0次
Date& operator+=(int day);
//+复用+=
//拷贝2次
Date operator+(int day) const;
//前置++:拷贝0次
Date& operator++();
//后置++,为什么是int呢?,C++创始人定的语法,没有为什么
//拷贝2次
Date operator++(int);
//前置++拷贝0次,后置++拷贝2次,因此前置++更好
Date& operator-=(int day);
Date operator-(int day) const;
int operator-(const Date& d) const;
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
#include "Date.h"
void Date::Print() const
{
cout << _year << " " << _month << " " << _day << endl;
}
Date::Date(int year, int month, int day)
{
//对传入的年份进行检查
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;
}
Date::Date(const Date& d1)
{
//对传入的年份进行检查
if ((d1._year < 0) || (d1._month < 1 || d1._month>12) || (d1._day<1 || d1._day>GetMonthDay(d1._year, d1._month)))
{
//assert(false);//粗暴一点的方法
Print();//本质是:this->Print();
cout << "拷贝对象的日期为非法日期" << endl;
cout << "无法进行拷贝" << endl;
exit(-1);
}
_year = d1._year;
_month = d1._month;
_day = d1._day;
}
Date& Date::operator=(const Date& d)
{
//对传入的日期进行检查
if ((d._year < 0) || (d._month < 1 || d._month>12) || (d._day<1 || d._day>GetMonthDay(d._year, d._month)))
{
//assert(false);//粗暴一点的方法
cout << "拷贝对象的日期为非法日期: ";
cout << "无法进行拷贝" << endl;
exit(-1);
}
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
bool Date::operator==(const Date& d) const
{
return _year == d._year && _month == d._month && _day == d._day;
}
bool Date::operator!=(const Date& d) const
{
return ((*this) == d);
}
bool Date::operator>(const Date& d) const
{
if (_year > d._year)
{
return true;
}
if (_year == d._year && _month > d._month)
{
return true;
}
if (_year == d._year && _month == d._month && _day > d._day)
{
return true;
}
return false;
}
bool Date::operator<=(const Date& d) const
{
return !((*this) > d);
}
bool Date::operator>=(const Date& d) const
{
return (*this) > d || (*this) == d;
}
bool Date::operator<(const Date& d) const
{
return !((*this) >= d);
}
//返回year年month月有多少天
int Date::GetMonthDay(int year, int month)
{
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)))
{
MonthArray[month]++;
}
return MonthArray[month];
}
//上面这个+复用+=更好
//拷贝0次
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)
{
_month = 1;
_year++;
}
}
return *this;
}
//+复用+=
//拷贝2次
Date Date::operator+(int day) const
{
Date d1(*this);
d1 += day;
return d1;
}
//前置++:拷贝0次
Date& Date::operator++()
{
*this += 1;
return *this;
}
//后置++,为什么是int呢?,C++创始人定的语法,没有为什么
//拷贝2次
Date Date::operator++(int)
{
Date d(*this);
//+=已经考虑了不同月份日期不同
*this += 1;
return d;
}
//前置++拷贝0次,后置++拷贝2次,因此前置++更好
Date& Date::operator-=(int day)
{
//减一个负数==加上这个负数的相反数
if (day < 0)
{
return (*this) += -day;
}
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_month = 12;
_year--;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date::operator-(int day) const
{
Date d(*this);
d -= day;
return d;
}
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1;//假设:*this大于d,左大于右 左-右
//假设错误
if (min > max)
{
max = d;
min = *this;
flag = -1;//右大于左
}
int day = 0;
while (min < max)
{
//++min;//++min是代码实现是调用+=的,那我直接调用+=不好吗,更快
min += 1;
day++;
}
return flag * day;
}
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
以上就是
C++类和对象中:运算符重载+const成员函数+日期类的完善
的全部内容,
希望能对大家有所帮助!