目录
1.赋值运算符重载
1.1 运算符重载
1.2赋值运算符重载
1.3引用作为返回参数☆☆
1.4深入赋值运算符重载
2.实现日期Date类
2.1类之间的运算符重载
2.1.1相等
2.1.2小于
2.1.3复用实现其他
2.2类与整形之间的运算符重载
2.3单目操作符的重载
3. 流插入、流提取的重载
4.const成员(关注权限问题)
5.取地址操作符的重载
6.完整代码
(文章末尾有完整代码)
1.赋值运算符重载
1.1 运算符重载
C++ 为了增强代码的可读性引入了运算符重载 , 运算符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字 operator 后面接需要重载的运算符符号(有点类似于将operator+符号名作为一个“函数名”) 。
operator+运算符构成函数名,任然用熟悉的Date类,示例如下:
bool operator<(const Date& dt1, const Date& dt2)
{
if (dt1._year < dt2._year)
{
return true;
}
else if (dt1._year == dt2._year)
{
if (dt1._month < dt2._month)
{
return true;
}
else if (dt1._month == dt2._month)
{
return dt1._day < dt2._day;
}
}
return false;
}
(由于运算符优先级的问题,d1 和 d2之间需要打上括号)
............................
除了作比较,我们是否可以对一个两个日期作差,或者对日期++ --呢?
我们都能明白这是个什么意思,但是目前的编译器还不太明白,所以需要我们通过运算符重载来实现
各种运算的返回值:
注意:
1.不能通过连接其他符号来创建新的操作符:比如 operator@2.重载操作符必须有一个类类型参数(如:int operator-(int i,int j),系统不希望你通过关键字operator改变-对两个内置类型int的操作)3.用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不能改变其含义4.作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐藏的 this5. .*(点星) :: sizeof ?: . 注意以上 5 个运算符不能重载。这个经常在笔试选择题中出现
重载好之后,也可以有很多种调用方法:
很明显,转换调用更好用。
那么, 那些运算符需要重载呢?
一个类需要重载哪些运算符,是按照是否有重载价值和重载需求来判断的
新的问题:
上文中的重载部分的代码块中,我们都将类中的被private修饰的变量放开了,否则无法访问
有三种解决方案:
1.提供成员的get和set(自己写一个成员函数)
2.友元,会在之后讲解
3.将operator对应的函数重载为成员函数
此处我们着重说明第三种,如果在成员函数中写:
bool operator==(const Date& d1,const Date& d2){
return d1.year==d2.year&&
d1.month==d2.month&&
d1.day==d2.day
};
那么就会出现报错,因为参数个数应该和操作数个数一致 ,==是一个双目操作符,而加上隐藏的this指针作为形参,一共有三个参数,明显不一致。
所以我们需要改造一下这个函数重载的写法:
class Date {
public:
Date(int year = 1900, int month = 1, int day = 1);
bool operator==(const Date& d) {
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
有两种调用办法:
转换调用:
编译器会先在类中找,再在全局中找,如果找不到就会报错。
因此,如果类和全局中都有operator==,会优先使用类中的。
再在汇编角度看一下两种调用方法:
底层是一样的。
自然地,更多的使用转换调用。
注意:在转化调用中,假如有两个操作数,第一个操作数会作为this 第二个会作为参数。
在==中体现不多,但是在< > -中就有区别了。
1.2赋值运算符重载
赋值重载,即赋值拷贝,也就是将一个已经存在的对象赋值给另一个已经存在的对象。
两个对象都是已经存在的!因此不要与拷贝构造混淆。
//拷贝构造
Date d3=d1;
//赋值重载
Date d4(1999,11,9);
d1=d4;
同上写法:
进一步的,为了支持连续的赋值:
d1=d2=d3;
我们也可以在类中将赋值运算符写为:
Date operator=(const Date& d);
给重载的赋值运算符一个返回值,就像原本的整数间的赋值运算符一样。
1.3引用作为返回参数☆☆
为了更详细的学习、了解赋值运算符的重载,我们先学习一下引用作为返回参数的一些知识。
传值返回会生成当前对象的拷贝,会拷贝一个临时对象来作为函数的返回值
Date func(){
Date d(2024,4,14);
return d;
}
而传引用返回不会去拷贝,但是这样的写法是不正确的。因为出函数的时候d就已经被销毁了
Date& func(){
Date d(2024,4,14);
return d;
}
当返回对象作为一个局部对象或临时对象,出了函数作用域就会调用析构函数,因此也不能直接这样使用引用返回。 因此,传值返回虽然多拷贝一次,但是能正确返回。
可以用静态区变量解决这个问题。
在函数返回值的接受处也应当注意(用静态区开辟后引用返回时):
对于这两种接受办法,右侧的ref在接受时还会再拷贝一次,左侧不会再拷贝。
但是如果使用传值返回,还要注意接收处的权限问题,需要加const来适应临时拷贝所具有的常性。
tips:所有的传值返回,传回的值都会拷贝给临时变量,而临时变量具有常性,因此大部分的传值返回都要注意临时变量的常性带来的权限问题。
▲分析四种经典的与引用有关的返回:
1. Date返回,Date接受:
最纯粹、最简单的返回:用值返回,开新空间接受值,也是曾经各位同学用的最多的返回模式。其本质为:函数func返回的是d的临时拷贝,临时变量(临时拷贝)是存放在当前函数栈帧的,也就是说d的拷贝是存放在main函数中的。func返回了d的拷贝之后,ref1开辟出一个新的空间,在该新空间中拿到该临时变量的值(也就是将临时变量又拷贝到了ref1的新空间中去),因此,使用这样的用值返回、用值接受时,没有权限问题,func接受到的也是一组独立的完整的数据,不用担心销毁、覆盖一类的问题。
2.Date返回,Date&接受:
func返回的是d的临时拷贝,临时拷贝存储在main的函数栈帧中,并且具有常性,ref1直接作为该临时拷贝的别名,能修改和阅读该临时拷贝,属于权限的放大,因此报错。
加上一个const,就不会报错了:
因为ref1是临时拷贝的别名,临时拷贝的生命周期与main一致,不会因func的销毁而被占用,所以此时的ref也是非常安全的,其内容是不会被覆盖的。
3.Date& 返回,Date&接受
最危险,最容易出错的一种写法。
ref1相当于是d的引用、是d的别名。但,d是在func函数栈帧中的变量,函数栈帧销毁了,这一块空间是可以被覆盖的,数据与内容也是可以被清理的(取决于编译器),比如我们在
出了func空间之后再执行一个简单的fx()函数,就能观察到,ref1所代表的空间中的内容已经
被修改了:
4.Date& 返回,Date 接受
返回的是d的别名,并且ref1在一个新空间中通过d的别名拿到d的值(也就是通过d的别名将d的值拷贝给ref1).
d不是已经被销毁了吗?
由于编译器只是销毁了该栈帧空间(将该空间还给操作系统),但是d所对应的区域的值还没有被覆盖、改变,所以ref1成功拿到了d的值的拷贝。由于ref1中拿的是复制过的值,所以ref1中的数据也很安全,不会被覆盖。
总而言之:
当然,引用返回在很多场合下可以减少拷贝,效率更高。
只要引用对象生命周期没有结束,就应该使用引用返回
1.4深入赋值运算符重载
回到刚才的赋值运算符重载的问题:
在类中实现赋值运算符重载时,就可以返回(*this)的引用,减少在返回时的拷贝次数。
(不用担心权限问题,例如d1=d2=d3; 其中的d1/d2/d3都是已经初始化好了的变量。d2=d3的返回值就是d2的引用,d2的引用作为d1赋值运算符的右操作数)
同时,如果赋值重载中涉及到使用深拷贝时,如果执行:
st1=st1;//栈中有malloc出的资源
浪费、消耗就非常大,所以我们再稍微处理一下:
Date& operator=(const Date& d) {
if (this != &d) {
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
这样就更加完美了。
既然作为类中的默认成员函数,当我们没有显式实现运算符重载时,编译器会自动生成,性质与默认拷贝类似。需要深拷贝的,依然需要我们自主实现,编译器生成的只能实现浅拷贝
最后:赋值运算符重载不能实现在全局
提问:拷贝构造中也有“=”的写法,赋值运算符中也有“=”的写法,是不是实现一个就可以了呢?
欢迎留言评论区。
2.实现日期Date类
我们以日期为例讲解了类和对象中的各种知识,现在我们来真正实现这个类
2.1类之间的运算符重载
2.1.1相等
bool Date :: operator==(const Date& d) {
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
我们选择将函数的实现和声明分离(在Date.cpp文件中实现,在Date.h文件中声明)
直接将函数定义在类中,默认其为inline函数;如上分开实现,则不会默认其为inline函数。
2.1.2小于
bool Date::operator<(const Date& d) {
if (_year < d._year) {
return true;
}
else {
if (_year == d._year) {
if (_month < d._month) {
return true;
}
else {
if (_month == d._month) {
if (_day < d._day) {
return true;
}
}
}
}
}
return false;
}
在使用<时要注意,左操作数是this,右操作数是d
先传隐藏参数this,再传d,所以执行起来就是 *this<d
2.1.3复用实现其他
在实现了相等、大于、小于之后,当然可以通过直接cv逻辑再改符号,不过我们更加推荐复用的办法解决,这也是所有需要作比较的类在实现运算符重载时通用的实现思想。
bool Date :: operator<=(const Date& d) {
if (*this < d || *this == d)
return true;
return false;
}
bool Date :: operator>(const Date& d) {
if (!(*this <= d))
return true;
return false;
}
bool Date :: operator>=(const Date& d) {
if (!(*this < d))
return true;
return false;
}
注意:this是指针,需要对this解引用来获取该对象
2.2类与整形之间的运算符重载
之前我们说到,只要有一个操作数是自定义类型,就可以实现运算符重载
除了双目操作数,还有日期类与int(天数)作加减。竞赛中,类似的计算日期的题目经常作为签到题存在,我们通过加法计算进位的思想来实现他们,包括+= -= + -
+= :
我们通过GetMonthDay来获得当年当月的天数(年份的目的主要是应对二月是否为闰月)
Date& Date :: operator+=(int day) {
this->_day += day;
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13) {
_year++;
_month = 1;
}
}
return *this;
}
实现GetMonthDay:
使用较频繁的函数直接放在该class对应的公共代码段作为inline函数,因为会多次调用,这样可以省去开栈帧的过程,例如此处的GetMonthDay,而之前的 opretator>= 等,由于使用相对较少,就可以放在.cpp文件中(定义和声明相分离),需要调用时编译器自动建立栈帧即可。
同理 -= - +
Date Date:: operator+(int day) {
Date tmp = *this;
tmp += day;
return tmp;
}
为了方便,我们任然采用复用的思维,但是此时的tmp是临时变量,由之前的知识得,不能再用引用作为返回值,而是需要执行一次拷贝,传值返回。
如果我们先实现+,也可以做到用+实现+=:
+优先级更高,先调函数让*this和day作为参数进入函数,再将函数的返回值通过赋值运算符的重载赋值给*this.
想一想,用+复写+=更好,还是用+=复写+更好?
用+=复写 +更好,因为+的实现是传值返回,如果用+复写+=,明明不需要拷贝的的+=也会经历拷贝的过程。
对于-和-=,我们依然采用借位的办法:
先在day上直接做减法,只要小于等于0就借位,注意借位借的是上一个月的天数。
Date& Date :: operator-= (int day) {
this->_day -= day;
while (_day <= 0) {//等于0也是不可以的,因为不存X月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;
}
此时的功能还不全面,如果一个日期+=-40,程序将出错。所以应当在两个被复用(+= -=)重载中加上判断。毕竟,重载的目的是增强代码可读性。
多个同一运算符重载可以构成函数重载,如下图(相同的运算符,不同的运算符参数,同样的函数名,不同的参数构成函数重载)
2.3单目操作符的重载
以上两种-尚且存在不同的参数,若参数名等全部相同呢?
//单目操作数
//++d1
Date& operator++();
//d1++
Date& operator++();
这是一种规定,所有的类中的后置++和后置--,都用int来占位
编译器的工程师会直接按照这种规定实现相关的映射,不要纠结于两种方法是如何联系的,为什么一个this在前面、一个this在后面等问题。
代码实现如下:
Date& Date::operator++() {
//没有int,是前置
*this += 1;
return *this;
}
Date Date::operator++(int) {
Date tmp = *this;
++*this;
return tmp;
}
▲:
后置++若还是使用的是Date&作返回,若使用Date接受还好(还是有风险,毕竟tmp对应的空间还给操作系统了),可以将数据拷贝进新的Date,倘若用Date&接受,就变成了上文中我们书写的最不安全 的一种写法。
所以,在自定义类型中 ,我们更推荐使用前置++和前置--,因为会减少拷贝的次数(拷贝了就需要开空间和析构,对于此处的日期类尚且还好,尤其是对于需要深拷贝的对象,代价就大得多了)
日期类作差:
因为我们已经实现了常规的+-等操作,所以直接让小日期作加法到达大日期,计数一共加减多少次即可。日期之间的数字相对计算机一秒钟上亿次的计算还是小问题。
int Date::operator-(const Date& d) {
Date max = d;
Date min = *this;
if (max < min) {
max = *this;
min = d;
}
int ans = 0;
while (min < max) {
//习惯多用前置
++min;
++ans;
}
return ans;
}
(这个不是单目操作符,但是有了前文才能足够好的理解)
★补充:在这种多文件的项目中,如GetMonthDay这种成员函数直接实现是不会报错的(他被视作inline,会特殊处理,不进符号表,不用担心连接问题),否则会因为在test.cpp和Date.cpp中都被展开而报重复定义的错,我们通过静态区的方法解决:
3. 流插入、流提取的重载
在之前(包括C语言阶段),如果我们想查看写好的类的内容,我们需要自主写一个Print函数来实现。
C语言不支持:
C++提供的符号重载,让我们有机会用流插入和流提取来按照我们的意愿打印一个对象。
为什么内置类型都能自动识别并且被输出?其本质也是重载
内置类型都是提前被重载实现好了的:
为了兼容C语言,C++将cpp和c的输入和输出混合兼容,这也是为什么cpp的输出相对较慢:
日期类型的流插入、流提取:
ostream(out_stream,也就是输出流)中有一个叫console_out(cout)类型,同理istream中有一个cin.
这样处理变量顺序会反,因为函数传参默认第一个参数是this ,使用起来就变成了
d.operator<<(cout),也就是d<<out
因此,我们将其实现为全局重载,就可以自由控制参数顺序:
此时成员变量被private修饰,不能被访问
并且现在还无法连续输出:
从左向右写,每运行一次重载,就把这个运算符的值返回为ostream,也就是我们的cout(因为cout是作为引用被传入,out是cout的别名,传引用返回就相当于把cout返回回去了,能让d2继续和cout执行重载过后的<<)
现在通过友元解决private修饰问题:
我们在class中加入语句:
friend ostream& operator<<(ostream& out, Date& d);
“我是你的朋友,我能去你家玩”,这样,我们重载的<<就能访问该类中的元素。
友元函数声明可以放在共有或者私有中
ostream& operator<<(ostream& out,const Date& d) {
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d) {
cout << "请输入合法的年月日:" << endl;
in >> d._year >> d._month >> d._day;
if (!(d.CheckDate(d._year, d._month,d._day))) {
cout << "日期非法" << endl;
}
return in;
}
4.const成员(关注权限问题)
将 const 修饰的 “ 成员函数 ” 称之为 const 成员函数 , const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中 不能对类的任何成员进行修改。
将一个只读的数据传给一个可读可写的函数参数,也算是权限的放大
d1是const Date ,传出去的是const Date* const this
Print的参数(this)是 Date* const this
(因为this本身是不允许被修改的,所以Print中原本有个限制指针的const)
用函数名后置的const来解决:
this本身是Date* const this(指向的空间地址不能改变的指针),这样修饰之后就是const Date* const this(双const,不能改变指向,不能改变指向的内容)
形参处不能写this,自然就无法在形参处对this进行修饰,所以后置的const是一种对逻辑不闭环打的补丁
因此,为了让我们实现的函数以及重载都能对被const修饰过的变量进行操作,我们可以给大部分的函数加上一个后缀的const(声明和定义处都要加),这样既能操作如上图的d1,也能操作如上图的d2(缩小函数的权限,让低权限和高权限的变量都能被使用).
当然,也不是所有的都适合使用后缀const:
比如+= -=的重载就不能在函数名之后加const。 因为+=或则-=是需要对其对应的this作出修改的
答案为:不可以、可以、不可以、可以
5.取地址操作符的重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。意义不大,但是可以用来返回假地址,返回你希望对方得到的地址。
6.完整代码
//.h头文件
#pragma once
#include <iostream>
#include <cstdlib>
#include <assert.h>
using namespace std;
static int month_day[13] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
class Date {
friend ostream& operator<<(ostream& out,const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1900, int month = 1, int day = 1);
void Print() const;
~Date();
bool CheckDate(int year, int month,int day)const;
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;
//常用的函数直接在类内部实现
int GetMonthDay(int year, int month) const {
assert(month <= 12 && year > 0);
if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) {
return 29;
}
return month_day[month];
}
Date& operator+= (int day);
Date operator+ (int day)const;
Date& operator-= (int day);
Date operator- (int day)const;
//单目操作数
//++d1
Date& operator++();
//d1++
Date operator++(int);
//日期之间作差
int operator-(const Date& d)const;
private:
int _year;
int _month;
int _day;
};
//.cpp文件
#define _CRT_SECURE_NO_WARNINGS
#include "Date.h"
Date::Date(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
bool Date :: CheckDate(int year, int month,int day)const {
if (year < 0 || month < 0 ||
month >= 13 ||GetMonthDay(year, month) < day ||
day < 0) {
return false;
}
return true;
}
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);
}
Date& Date::operator=(const Date& d) {
if (this != &d) {
this->_year = d._year;
this->_month = d._month;
this->_day = d._day;
}
return *this;
}
bool Date::operator<(const Date& d)const {
if (_year < d._year) {
return true;
}
else {
if (_year == d._year) {
if (_month < d._month) {
return true;
}
else {
if (_month == d._month) {
if (_day < d._day) {
return true;
}
}
}
}
}
return false;
}
bool Date :: operator<=(const Date& d)const {
if (*this < d || *this == d)
return true;
return false;
}
bool Date :: operator>(const Date& d)const {
if (!(*this <= d))
return true;
return false;
}
bool Date :: operator>=(const Date& d) const{
if (!(*this < d))
return true;
return false;
}
Date& Date :: operator+=(int day) {
this->_day += day;
while (_day > GetMonthDay(_year, _month)) {
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13) {
_year++;
_month = 1;
}
}
return *this;
}
Date Date:: operator+(int day)const {
Date tmp = *this;
tmp += day;
return tmp;
}
Date& Date :: operator-= (int day) {
this->_day -= day;
while (_day <= 0) {//等于0也是不可以的,因为不存X月0号的说法
_month--;
if (_month == 0) {
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date Date :: operator-(int day)const {
Date tmp = *this;
tmp -= day;
return tmp;
}
Date& Date::operator++() {
//没有int,是前置
*this += 1;
return *this;
}
Date Date::operator++(int) {
//有int 是后置
Date tmp = *this;
++*this;
return tmp;
}
int Date::operator-(const Date& d)const {
Date max = d;
Date min = *this;
if (max < min) {
max = *this;
min = d;
}
int ans = 0;
while (min < max) {
//习惯多用前置
++min;
++ans;
}
return ans;
}
Date::~Date() {
_year = -1;
_month = -1;
_day = -1;
}
void Date::Print() const
{
cout << _year << "-" << _month << "-" << _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) {
cout << "请输入合法的年月日:" << endl;
in >> d._year >> d._month >> d._day;
if (!(d.CheckDate(d._year, d._month,d._day))) {
cout << "日期非法" << endl;
}
return in;
}