深入篇【C++】类与对象:运算符重载详解
- ⏰一.运算符重载
- 🕓1.<运算符重载
- 🕐2.>运算符重载
- 🕒3.==运算符重载
- 🕑4.=运算符重载
- ①.格式
- 1.改进1
- 2.改进2
- ②.默认成员函数
- 1.功能
- 2.不足
- 🕓5.<=运算符重载
- 🕒6.>=运算符重载
- 🕑6. !=运算符重载
- 🕐7.+=运算符重载
- 🕦8.+运算符重载
- 🕚9.-=运算符重载
- 🕗10.-运算符重载(1)
- 🕖11.-运算符重载(2)
- 🕕12.前置++运算符重载
- 🕔13.后置++运算符重载
- ⏰二.日期类的实现
⏰一.运算符重载
内置类型(int /double…… )是可以之间进行运算符之间的比较的,因为编译器知道它们之间的比较规则,可以之间转化为指令。
那如果自定义类型能否之间进行运算符之间的比较呢?当然不能了,因为编译器是不知道这个自定义类型的规则是什么,不知道如何进行比较。
1.内置类型是可以之间比较的。
2.自定义类型是无法之间进行比较的。
那如何使自定义类型也能进行比较呢?这时C++给出了办法,让这个运算符重载成一个函数,当自定义类型进行比较时,其本质就是在调用重载函数。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有器返回值类型,函数名字以及参数列表,其返回值类型与参数列表和普通函数类似。
函数的名字:关键字operator后面加需要重载的运算符符号。
函数的原型为:
返回值类型 operator运算符符号(参数列表)
【注意】
- 不能通过连接其他符号来创建一个新的操作符。比如:operator#
- 重载函数参数必须至少有一个为自定义类型参数,不能全是内置类型参数。
- 用于内置类型的运算符,其含义是不能改变的。原本的运算符对内置类型的含义是不变的。
- 重载函数作为成员函数时,其形参看起来要比操作数目少一个,那是因为成员函数的第一个参数默 认为this指针。
- *. / sizeof / :: / ? : / .`这五个运算符是不能被重载的。
那所有的运算符都可以重载吗?
当然不是,哪些运算符能够重载呢?–>哪些对类有用的运算符就可以实现重载。
我们先写一个日期类的对象:
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year= year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
}
下面我们将对这个日期对象进行运算符之间的比较,比较两个日期的是否大于或者小于。
🕓1.<运算符重载
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year= year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool Less(const Data& x1, const Data& x2)
{
if (x1._year < x2._year)
{
return true;
}
else if (x1._year == x2._year && x1._month < x2._month)
{
return true;
}
else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
{
return true;
}
return false;
}
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
Less(d1,d2);
}
```c
我们写这个Less函数是用来比较第一个对象是否小于第二个对象的,这种写法跟我们以前写的函数是差不多的,不过有时候这种写法会出现问题,那就是比较专业的人会按照功能给函数命名,而不是很专业的人呢,就会乱起名字,导致我们可能不知道这个函数是干什么用,这里函数Less一看就知道是看是否小于了,如果写成别的可能就认不出来了,所以C++就给出了运算符重载形式,统一运算符,让运算符既可以对内置类型起作用,又可以对自定义类型起作用,这样又方便,又好识别。
C++规定运算符重载的写法如下:
返回值 operator>(参数)
所以上面的Less函数就可以写成这样的运算符重载函数:
bool operator<(const Data& x1,const Data& x2)
{
if (x1._year < x2._year)
{
return true;
}
else if (x1._year == x2._year && x1._month < x2._month)
{
return true;
}
else if (x1._year == x2._year && x1._month == x2._month && x1._day < x2._day)
{
return true;
}
return false;
}
这个函数就是对<运算符进行重载,使这个运算符<可以被自定义类型使用,但注意的是,<运算符对内置类型的运算规则没有改变,仍然遵守原先的。
也就是当自定义类型使用这个<运算符时,编译器会自动跳到这个运算符重载函数里。
自定义类型使用运算符比较本质上就是在调用函数。
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
//< 运算符重载之后,自定义类型就可以使用这个运算符了。
//两个自定义类型之间的比较就可以直接这样写:
d1<d2;
//而自定义类型使用运算符本质上就是去调用函数:本质上就是这样:
operator(d1,d2);
//这两种写法都是一样的。
}
只不过这里有个问题,那就是该重载函数写在类外,无法访问类里面的私有成员,得先将类里面的成员变成公有的才可以访问。那么问题来咯,封装性如何保证?
【解决方法】
将该运算符重载函数直接写到类里面,变成成员函数。那样就可以直接访问成员变量了。
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
bool operator<(const Data& x)//参数只有一个,还有一个传给隐藏的this了
{
if (_year < x._year)
{
return true;
}
else if (_year == x._year && _month < x._month)
{
return true;
}
else if (_year == x._year && _month == x._month && _day < x._day)
{
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
不过写成成员函数要注意一点就是,成员函数会默认将将调用的对象传给隐藏的this指针,而参数看起来少了一个,其实不是,只是将调用的对象传给隐藏的this了。所以在写成成员函数时,当操作数有两个时,那么参数就只有一个。
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
d1<d2;
//运算符重载函数写成成员函数后,那么对应的函数调用也就不一样了
//原来 d1<d2 === operator<(d1,d2);
//现在 d1<d2 === d1.operator(d2);
}
🕐2.>运算符重载
有了上面的<运算符重载,这个>运算符重载和它是一样的。
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year= year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool operator>(const Data& x1, const Data& x2)//运算符重载
{
if (x1._year > x2._year)
{
return true;
}
else if (x1._year == x2._year && x1._month > x2._month)
{
return true;
}
else if (x1._year == x2._year && x1._month == x2._month && x1._day > x2._day)
{
return true;
}
return false;
}
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
d1>d2;
//等同于
operator>(d1,d2);
}
写到类外时就无法访问类里的私有成员,这是这里的问题,所以还是写到类里面来,弄成成员函数。
当写成成员函数时,就要注意参数只有一个。
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
bool operator>( const Data& x)//>运算符重载
{
if (_year > x._year)
{
return true;
}
else if (_year == x._year && _month > x._month)
{
return true;
}
else if (_year == x._year && _month == x._month &&_day > x._day)
{
return true;
}
return false;
}
private:
int _year;
int _month;
int _day;
};
并且要理解自定义类型使用>运算符时是在调用成员函数operator>(d2);
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
d1>d2;
//等同于
d1.operator>(d2);
}
🕒3.==运算符重载
//全局的operator==重载函数
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year= year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Data& d1, const Data& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
d1==d2;
//本质和下面一样
operator==(d1,d2);
}
当写成成员函数时:
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year= year;
_month = month;
_day = day;
}
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this,指向调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
要理解自定义类型使用>运算符时是在调用成员函数 operator= =(d2);
int main()
{
Data d1(2023,2,1);
Data d2(2023,5,6);
d1==d2;
//等同于
d1.operator==(d2);
}
🕑4.=运算符重载
赋值运算符重载就是对=运算符进行重载,使自定义类型也可以使用=运算符。
赋值就相当于将自己拷贝给对方,而它与拷贝构造函数的区别是什么呢?
1.运算符重载:是已存在的两个对象之间复制拷贝
2.拷贝构造函数:用一个已经存在的对象去初始化一个新对象
注意:下面的是拷贝构造而不是赋值运算符重载。
Data d1(2023,5,1);
Data d2=d1;
虽然用了=运算符,但是记住赋值运算符重载的条件是两个已经存在的对象之间进行赋值拷贝,而d2是新创建的对象。
①.格式
- 参数类型:引用传参,用const修饰,即const 类&
引用传参可以提高传参效率。 - 返回值类型:引用返回,即 类&
引用返回可以提高返回的效率,有返回值目的是为了支持连续赋值功能。 - 要检查是否给自己赋值
- 返回*this:要符合连续赋值的含义。
重载函数形式:返回值 operator=(参数)
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
void operator=(const Data& d)//赋值运算符重载
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2023, 5, 1);
Data d2(2023, 5, 6);
d1 = d2;
}
我们可以发现将赋值运算符重载后,自定义类型也可以使用赋值运算符了,而且使用的很成功,将d2的值赋给了d1。
并且我们要理解自定义类型使用运算符本质是在调用函数,也就是可以写成这样:
int main()
{
Data d1(2023, 5, 1);
Data d2(2023, 5, 6);
d1 = d2;
//本质是调用函数,这两者是一样的
d1.operator=(d2);
}
那这样就没有问题了吗?
我们知道在对内置类型进行赋值时我们可以连续赋值比如这样:
int a,b,c;
a=b=c=0;
赋值的顺序从右到左,0先赋值给c,然后得到一个结果,再赋值给b,得到一个结果,再赋值给a。
那自定义类型能否完成这样的连续赋值操作呢?
即这样:
Data d1(2023, 5, 1);
Data d2(2023, 5, 6);
Data d3(2023, 9, 9);
d1=d2=d3;
我们发现这样的操作编译器是无法编译通过的,为什么呢?
这是因为当调用运算符重载函数时,返回值是void,而调用完的结果要再赋值给d1,而void和d1的类型无法匹配。
而要支持上面的连续赋值,需要将赋值重载函数的返回值写成调用该函数的类型,这样当对象调用完后,返回的仍然是同类型,这样就可以支持连续赋值了。
class Data
{
public:
Data(int year = 2023, int month = 5, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
Data operator=(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Data d1(2023, 5, 1);
Data d2(2023, 5, 6);
Data d3(2023, 9, 9);
d1 = d2 = d3;
1.改进1
这样我们的自定义类型就可以很好的使用赋值运算符啦,接下来我们来对这个赋值运算符重载函数进行一些改进,哪些地方可以改进呢?
我们的参数是引用传参,这样可以减少拷贝,提高效率,那该函数的返回值是否可以用引用返回呢?
思考一下,我们返回的是什么?我们返回值的是调用对象,即this指针指向的对象。
而当该函数结束时,this指针会销毁,但this指针指向的对象还在呀,该对象并不在运算符重载函数里面
(*this是在该函数里面,作为形参压入函数栈帧)。
所以我们可以用引用返回,这样就可以减少拷贝,提高效率啦。
Data& operator=(const Data& d)//用引用返回,可以提高效率
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
2.改进2
我们要求不要对自己赋值,所以在赋值之前我们可以进行检查,看是否对自己赋值,只要检查两个对象的地址是否相同即可:
Data operator=(const Data& d)
{
if (this != &d)//如果两个对象的地址不相同那么就可以进行赋值
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
}
②.默认成员函数
赋值运算符也是作为默认成员函数的。
而默认成员函数有一个规定:只能写在类里面,不能写在类外面
(但是可以在类里面声明,在类外定义,声明和定义分开)
为什么呢?
因为默认成员在用户不显示写时,它会自动生成一个默认函数,帮你完成赋值工作。
而当你在类外写赋值重载函数时,编译器会认定你没有写,它会自动生成一个默认函数,那这样就和编译器生成的运算符重载冲突了,所以赋值运算符只能写在类里面。
总结:
赋值运算符不显示写,编译器会生成一个默认的,如果用户再在类外自己实现一个全局的赋值运算符重载,就和编译器生成的默认函数冲突了,故赋值运算符重载只能是类的成员函数。
1.功能
默认成员函数的意义就是,用户不显示实现,编译器会自动生成一个默认的函数,来帮你实现。
那编译器生成的默认函数的具体是如何实现的呢?它的具体功能是什么呢?
当用户不显示实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝,即浅拷贝。
即默认生成赋值运算符重载与默认生成的拷贝构造函数行为一致:
1.内置类型成员是之间赋值–值拷贝/浅拷贝。
2.自定义类型成员会去调用对应的它的赋值运算符重载,如果没有那也是浅拷贝。
class Time//时间类
{
public:
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date//日期类
{
private:
// 基本类型(内置类型)
int _year = 2023;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
int main()
{
Date d1;
Date d2;
d1 = d2;
return 0;
}
我们先来分析一下该代码,该代码有两个类,一个是时间类Time,一个是日期类Data。
日期类里面的成员变量有内置类型又有自定义类型,并且没有写任何函数。
但我们知道编译器会给这个类自动生成一些默认函数,其中就有赋值运算符重载,接下来我们就看看,编译器生成的赋值运算符重载怎么样。
所以显而易见,对于这样日期类,时间类的,编译器自动生成的赋值运算符是可以满足使用的。
即编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝,这种已经符合部分类的需求了,那还要我们自己实现吗?
2.不足
当类成员涉及有关资源的开辟时,编译器生成的默认成员函数就无法使用。
typedef int DataType;
struct stack//class可以定义一个类
{
public://访问限定符
stack(int capacity = 4)//缺省值
{
cout << "stack(int capacipty=4)" << endl;
_array = (DataType*)malloc(sizeof(DataType) * capacity);
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
{
return;
}
--_size;
}
int Empty()
{
return _size == 0;
}
~stack()
{
cout << "~stack()" << endl;
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private://访问限定符
DataType* _array;
int _capacity;
int _size;
};
int main()
{
stack s1;
stack s2;
s2=s1;//使用编译器自动生成的赋值运算符重载函数来赋值
}
这样会出现什么问题呢?上一篇拷贝构造函数其实就讲了,因为它和拷贝构造函数的功能类似,所以遇到的问题也是类似。
因为编译器生成的默认函数都是浅拷贝,对于一些开辟了空间资源的变量,浅拷贝是完完全全的拷贝,没有动过任何处理,所以这就涉及赋值后,两个对象里面的变量都指向同一块空间,都属于同一块区域。那么这问题就大了去了。因为会出现析构两次,和我改变你也改变,你改变我也改变的状态。
1.同一块空间会析构两次,会报错。
当s2对象生命周期结束时,系统自动调用析构函数来清理数据,那么*a指向的空间就被销毁了。
而当s1对象生命周期结束时,又析构一次相同的空间,这样同一块空间就析构两次了。
2.一个变量修改会影响另一个变量,因为两个变量都存在同一块空间里。
所以编译器生成的赋值重载函数是有不足地方的,不能全部都依赖编译器生成的函数。不过对于日期类的,MyQueue类的编译器都可以直接实现功能,对于Stack类的就不可以了。
1.所以如果类中没有涉及资源管理,赋值运算符是否实现都是可以的;
2.但是一旦涉及资源管理就必须自己实现。
class Data
{
public:
Data(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;
};
int main()
{
Data d1(2023,5,1);
Data d2(2023,10,15);
}
我们知道并不是所有的运算符都能重载的,主要是那些对类有意义的运算符才可以重载。
🕓5.<=运算符重载
两个日期进行比较大小,是有意义的,所以和<=运算符是可以重载的。而我在运算符重载详解 -(上)中已经将部分运算符介绍,接下来继续介绍运算符重载。
那
<=运算符重载
该如何写呢?
是不是直接可以在 <运算符重载
的基础上直接改动即可呀。
下面是<运算符重载函数
bool Data:: operator<(const Data& 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 Data:: operator<(const Data& 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.<= 是不是就是 小于或者等于呀,或者大于的逆命题。
2.>=是不是就是 大于或者等于呀或者就是小于的逆命题。
3.!=是不是就是 等于的逆命题呀。
bool Data::operator<=(const Data& d)//直接复用<运算符重载和==运算符重载
{
return *this < d || *this == d;
}
🕒6.>=运算符重载
.>=运算符重载可以复用 >运算符和等于==运算符重载
或者是<运算符重载的逆命题:
bool Data::operator>=(const Data& d)
{
return !(*this < d);//复用<运算符重载 ,<的逆命题就是>=
}
🕑6. !=运算符重载
= =的逆命题就是!=所以可以直接复用= =运算符重载
bool Data::operator!=(const Data& d)
{
return !(*this == d);//直接复用==运算符重载的 -- 逆命题
}
总结:所以我们在写这些比较运算符,习惯先将<运算符重载和==运算符重载先写出来,然后其他比较运算符直接复用这两个运算符重载即可。
🕐7.+=运算符重载
我们知道日期加上一个日期是没有什么意义的,但是一个日期加上一个天数,这就有意义了,可以知道该天数后是什么日期。所以日期类是允许+=运算符重载的。
那+=运算符如何重载呢?
我们首先要知道如何进行日期的计算,当天数大于当前月的最大天数时,天数就要减去当前月份的最大天数,然后月份就要进一。
注意点:
1.当天数很大时,月份不断进一,但要注意月份不能超过12,当月份再进一应该是1,而不是13.
2.要注意每个月份的天数是不同的,要考虑闰年的2月和其他年的2月天数也是不同的。
所以我们首先手搓一个获得不同月份的天数的函数,要考虑闰年和不同年的2月不同。
int Data::GetMonDay(int year, int month)
{
int monday[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)&&month==2)
{
return 29;
}
else
return monday[month];
}
有一个点提一下,这里可以改进。
这里我们要的是每个月的天数,但每次调这个函数我们都要进行一次闰年的判断,判断然后再看是不是2月,这样很麻烦,这里的if条件句的目的是为了获得2月的天数,如果不是2月那就不要再去判断闰年了,所以我们可以将判断是否是2月放在前面先判断就可以避免每次都要判断是否是闰年了。
然后还有我们要频繁的调用这个函数,也就是这个月份日期数组每次调用都会开辟,太浪费效率了,所以我们可以让它变成静态区的,开辟一次就可以啦。不用每次调用都开辟一次。所以改进后的代码如下:
int Data::GetMonDay(int year, int month)
{
static int monday[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) )//先判断是否是2月,如果不是直接走开。
{
return 29;
}
else
return monday[month];
}
接下来我们就要进行这个对+=运算符的重载了
//这里可以用引用返回喔,因为返回的是对象本身,对象本身并不在该重载函数里面,所以函数结束,对象还存在。
Data& Data::operator+=(int day)
{
//第一步将天数全部加起来
_day += day;
while (_day > GetMonDay(_year, _month))//当天数大于当月最大天数时,就要进行循环
{
_day -= GetMonDay(_year, _month);//要减去当月的最大天数
++_month;//然后月份++
if (_month == 13)//要考虑月份不能超过12,当月份变成13时这是,正确的是1月,并且年也要加1
{
++_year;
_month = 1;
}
}
return *this;//最后将日期对象返回
}
🕦8.+运算符重载
+运算符和+=运算符的区别是什么呢?
int b=2;
b+=1;
b+1;
+=将变量本身改变了,而+并没有将变量改变。
所以同理,对于自定义类型,+=也将自定义变量本身发生改变,而+并不会改变自定义变量本身。
+运算符重载函数和+=运算符重载函数的区别就在于它们的返回值不同。
虽然最后结果相同,但是+运算符重载返回值是一个临时拷贝的对象,并不是真正改变的对象。
+=运算符重载函数的返回值是真正改变的对象。
//这里不可以使用引用返回,因为返回的是拷贝对象,函数结束,拷贝对象就销毁了
Data Data::operator+(int day)
{
Data tmp(*this);//拷贝一份,让tmp返回,*this没有改变
tmp._day += day;
while (tmp._day > GetMonDay(tmp._year, tmp._month))
{
tmp._day -= GetMonDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13)
{
++_year;
tmp._month = 1;
}
}
return tmp;
}
我们注意到,+运算符重载函数里是让拷贝对象去实现+=运算符函数的相关操作,最后返回是拷贝对象
,真正的对象并没有修改。
这里我们也可以使用函数复用,+运算符重载直接复用+=运算符重载函数。
Data Data::operator+(int day) //+运算符重载 复用+=运算符重载
{
Data tmp(*this);//拷贝一份,让tmp返回,*this没有改变
tmp += day;//复用+=重载函数
这里其实就是tmp去调用+=运算符重载函数,tmp作为左参数。
return tmp;
}
其实+=运算符重载函数也可以直接复用+运算符重载
Data& Data::operator+=(int day)//用+=复用+
{
*this=*this + day;
//注意的是+是不修改本身的,所以对象变量+day还要再赋给对象变量
这里的+就是+运算符重载函数,*this对象去调用这个对象
return *this;
}
但是最好先实现+=运算符重载 再复用实现 +运算符重载,而不推荐用+=运算符重载来复用实现+运算符重载,为什么呢?
因为+运算符是不改变对象变量本身,所以必须要拷贝对象,也就是会调用拷贝构造函数,让拷贝对象去操作。并且最后返回的是拷贝对象,当函数结束时,该拷贝对象就销毁了,不能用引用做返回值。所以返回时又会调用一次拷贝构造构造。所以如果用+运算符重载来复用实现+=运算符重载,每调用一次+运算符重载该函数就要再调用两次拷贝构造。
而如果使用+=运算符重载来复用+运算符重载,就不需要调用拷贝构造了,因为+=运算符重载函数是会对对象本身修改,所以不需要拷贝对象,并且最后返回的是对象本身,可以用引用做返回,提高了效率哎,这不美滋滋?
所以呢,我们在写这类运算符时,最好先写+=运算符重载,然后再用+=运算符重载复用实现+运算符重载。
🕚9.-=运算符重载
有了日期加天数,那肯定也要有日期减天数呀。而且根据上面的介绍,我们是不是应该先从-=运算符重载写起,然后-运算符重载再复用-=运算符重载实现呀。
我们首先要分析,日期如何减一个天数
1.当日期的天数减去给定的天数结果小于等于0时,我们就需要对月改动,如果大于0那就不需要改动
2.当天数小于0时,我们需要借上个月的天数来还(注意是上个月,不是本月)
3.借着借着,要注意月份不能小于1了,当月份借到为0时,正确的应该是12月,并且年份要减一。
//跟+=运算符重载一样,这里-=运算符重载也可以使用引用做返回,因为最后返回的是对象本身,而不是对象的拷贝什么的。
Data& Data::operator-=(int day)
{
//首先将天数全部减去
_day -= day;
while (_day <= 0)//当天数小于等于0时,就要进循环来借上月份中的天数
{
--_month;//首先需要将月份减一,因为我们借的是上个月的天数,而不是本月的,本月没有天数辣,都用来减去天数变成负数了都,哪里的剩余天数。
if (_month == 0)//要注意月份不能小于1,当月份为0时
{
--_year;//年份要减一
_month = 12;//要将月份改成12
}
_day += GetMonDay(_year, _month);//借上个月份的天数来还。
}
return *this;
}
其实这个函数还有一点点问题,那就是下面这样:
int main()
{
Data d1(2023, 4, 26);
d1-=-10;
//日期减去一个天数,但这个天数却是负数,那结果是对的吗?
}
可以明显看出肯定错误了,因为天数怎么能超过31呢。
正确的场景是,如果天数为负数,那么-=天数应该变成了+=天数。
而+=天数就应该变成-=天数。
所以我们在-=运算符重载函数里再完善一点。
Data& Data::operator-=(int day)
{
if (day < 0)//如果天数小于0,那么-=天数就应该变成+=天数
{
return *this += -day;//这里的day还是负数所以加上负号让它变成正号。
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonDay(_year, _month);
}
return *this;
}
同理+=运算符重载函数里也应该这样写。
Data& Data::operator+=(int day)
{
if (day < 0)//如果天数小于0,那么+=天数就应该变成-=天数
{
return *this -= -day;//这里的day还是负数所以加上负号让它变成正号。
}
_day += day;
while (_day > GetMonDay(_year, _month))
{
_day -= GetMonDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
🕗10.-运算符重载(1)
有了-=运算符重载,我们还怕-运算符重载无法完成?笑话!
我们直接复用-=运算符重载即可,但要注意的是-运算符重载函数,需要拷贝对象,因为-运算符不修改对象本身喔。
Data Data::operator-(int day)
{
Data tmp(*this);//需要拷贝构造一个对象,让这个对象去操作
tmp -= day;//复用-=运算符重载
return tmp;
}
🕖11.-运算符重载(2)
日期-天数是有意义,可以知道天数之前是什么日期。
那日期-日期有没有意义呢?肯定有呀,这可以知道你小子从出生到现在活了多久。
这个就是对-运算符进行再重载。参数由天数(int类型)变成了,日期(日期类)。
因为函数名相同,但参数不同所以就可以构成重载。
那日期-日期如何计算呢?
data1 - data2 看起来很麻烦,涉及天数和月份甚至年份一起要改变。
不过我们可以不直接相减,换一种方法来求它们之间的天数。
1.只要让日期小的不断的加加到大的日期,每加一次就计数一次
2.当两个日期相同时,计数器所显示的就是它们之间的天数
3.不过要注意的是,一开始我们可不知道哪个日期大,哪个日期小,需要讨论。
int Data::operator-(Data& d)//这是日期-日期 -运算符重载函数
{
Data max = *this;//默认*this日期对象大
Data min = d;//d对象小
int flag = 1;
if (*this < d)//如果错了
{
max = d;
min = *this;
flag = -1;//那就将flag置为-1
}
int n = 0;//计数器
while (min != max)//让小的日期不断的加加到大的日期
{
++min;//每加一次
++n;//计数器计数一次
}
return n*flag;//最后计数器n就是它们之间的天数,而flag决定是正的还是负的。
}
🕕12.前置++运算符重载
C语言中++这个运算符分为前置++和后置++,前置++是先++再使用,而后置++是先使用后++。
在C++中如果要对自定义类型进行前置或者后置++,需要将它们重载成运算符函数。
C++规定前置++就正常按照运算符重载函数写,而后置++重载是要多增加一个int类型的参数,但是调用函数时该参数不用传递,编译器会自动传递。
//这里可以用引用做返回值,因为最后返回的是对象本身
Data& Data::operator++()//前置++运算符重载
{
*this += 1;//这里的+=其实就是+=运算符重载
return *this;
}
🕔13.后置++运算符重载
//这里不可以用引用左返回,因为返回的是拷贝对象,不是对象本身,函数结束,就销毁了
Data Data::operator++(int)//后置++运算符重载---比前置++多一个参数,为什么呢?因为这样才可以和前置++构成承载,因为函数名相同,参数不同才可以构成承载,因为不需要使用参数,所以可以只写类型
{
Data tmp(*this);//后置++,返回的是使用前的状态,因为后置++是先使用后++
*this += 1;//对象先使用,再++,也就是要将没++之前的对象返回过去。
return tmp;
}
⏰二.日期类的实现
TEST.h文件
#pragma once
#include <iostream>
using namespace std;
class Data
{
//析构也不用写
public:
Data(int year = 1, int month = 1, int day = 1)//构造函数要写,拷贝构造函数不用写,赋值重载不用写
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
Data(const Data& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
bool operator<(const Data& d);
bool operator==(const Data& d);
bool operator<=(const Data& d);
bool operator>(const Data& d);
bool operator>=(const Data& d);
bool operator!=(const Data& d);
Data operator++();//前置++
Data operator++(int);//后置++
int GetMonDay(int year, int month);
Data& operator+=(int day);
Data operator+(int day);
Data& operator-=(int day);
Data operator-(int day);
int operator-(Data& d);
private:
int _year;
int _month;
int _day;
};
```c
TEST.c文件
```c
bool Data:: operator<(const Data& 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 Data::operator==(const Data& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Data::operator<=(const Data& d)//直接复用上面的
{
return *this < d || *this == d;
}
bool Data::operator>(const Data& d)
{
return !(*this <= d);
}
bool Data::operator>=(const Data& d)
{
return !(*this < d);
}
bool Data::operator!=(const Data& d)
{
return !(*this == d);
}
int Data::GetMonDay(int year, int month)
{
static int monday[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };//频繁调用
/*if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)&&month==2)*/
if (month == 2&&(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) )
{
return 29;
}
else
return monday[month];
}
//Data& Data::operator+=(int day)//用+复用+=-最好先实现+= 再复用实现 +
//{
// *this=*this + day;
//
// return *this;
//}
Data& Data::operator+=(int day)
{
if (day < 0)
{
return *this -= -day;
}
_day += day;
while (_day > GetMonDay(_year, _month))
{
_day -= GetMonDay(_year, _month);
++_month;
if (_month == 13)
{
++_year;
_month = 1;
}
}
return *this;
}
Data Data::operator+(int day)
{
Data tmp(*this);//拷贝一份,让tmp返回,*this没有改变
tmp._day += day;
while (tmp._day > GetMonDay(tmp._year, tmp._month))
{
tmp._day -= GetMonDay(tmp._year, tmp._month);
++tmp._month;
if (tmp._month == 13)
{
++_year;
tmp._month = 1;
}
}
return tmp;
}
//Data Data::operator+(int day) //+ 复用+=
//{
// Data tmp(*this);//拷贝一份,让tmp返回,*this没有改变
// tmp += day;//复用+=重载函数
// return tmp;
//}
Data Data::operator++()//前置++运算符重载
{
*this += 1;
return *this;
}
Data Data::operator++(int)//后置++运算符重载
{
Data tmp(*this);
*this += 1;
return tmp;
}
Data& Data::operator-=(int day)
{
if (day < 0)
{
return *this += -day;
}
_day -= day;
while (_day <= 0)
{
--_month;
if (_month == 0)
{
--_year;
_month = 12;
}
_day += GetMonDay(_year, _month);
}
return *this;
}
Data Data::operator-(int day)
{
Data tmp(*this);
tmp -= day;
return tmp;
}
//d1 - d2
int Data::operator-(Data& d)
{
Data max = *this;
Data min = d;
int flag = 1;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n*flag;
}