C++第七节课 运算符重载
一、运算符重载
并不是所有情况下都需要运算符重载,要看这个运算符对这个类是否有意义!
例如:日期减日期可以求得两个日期之间的天数;但是日期 + 日期没有意义!
#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;
};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;}elsereturn false;
}
int main()
{Date d1(2023,11,20);Date d2(2023,10, 10);d1 < d2; //第一种调用形式operator<(d1 , d2); // 两种调用方法都可以(第二种调用形式)return 0;
}
在上述例子中我们对<进行重载,使其可以比较两个对象的日期大小!
d1<d2 和operator<(d1,d2)两者是等价的!编译器会将第一种调用形式转化为第二种调用形式;
从底层来看,两者执行的汇编指令是一样的!
但是这里有个问题,在类的外面不能访问private和protect!(上面是将private改为public)
因此,我们可以将函数写在类的内部,这样子方便调用!
但是,直接放入类中会报错:
这是因为:作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this!
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- .*(点星,这个运算符很少用) ::(域作用限定符) sizeof ?:(三目) .(成员访问) 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
使用成员函数进行运算符重载如下所示:
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1900, int month = 1, int day = 1){_year = year;_month = month;_day = day;}bool operator<(const Date& 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;}elsereturn false;}//private:int _year;int _month;int _day;
};int main()
{Date d1(2023,11,20);Date d2(2023,10, 10);d1 < d2;// operator<(d1 , d2); // 两种调用方法都可以d1.operator<(d2); // 此时通过成员函数的方式调用!(或者直接比较)return 0;
}
返回值类型为bool;
此时调用函数是采用成员函数的方法调用,且依然可以直接进行比较!(这里依然是两个参数,只是第一个this参数被隐藏了!)
接下来我们看两个操作之间的汇编代码,可以发现两者的操作是等价的!
传入的参数必须有一个自定义类型(因为我们不能对内置类型进行重载!)
二、赋值运算符重载
引入:
如果我们想把d2的值赋值给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;}bool operator<(const Date& 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;}elsereturn false;}void operator=(const Date& x){_year = x._year;_month = x._month;_day = x._day;}//private:int _year;int _month;int _day;
};int main()
{Date d1(2023,11,20);Date d2(2023,10, 10);d1 < d2;// operator<(d1 , d2); // 两种调用方法都可以d1.operator<(d2); // 此时通过成员函数的方式调用!(或者直接比较)// 将d2的值赋值给d1d1 = d2;d1.operator=(d2);cout << d1._year << endl;cout << d1._month << endl;cout << d1._day << endl;return 0;
}
函数的返回值类型为void!
通过结果发现可以完成对象之间的赋值!(对于日期来说我们需要的就是浅拷贝!)
小插曲:
对C语言来说,支持这样的连续赋值:将0赋值给k(整个返回式的结果为k),k赋值给j(整个返回式的结果为j),j赋值给i(整个返回式的结果为i);
那么两个对象是否可以这样赋值呢?
其实不可以!
自定义类型是转换为对应的两个函数调用,但是这里调用的返回值是void!
正确的操作应该是让右边的表达式返回d4!再将d4的值返回给d5!
因此我们修改下赋值运算符重载函数:
Date operator=(const Date& x){_year = x._year;_month = x._month;_day = x._day;return *this;}
例如d4 = d1,这里的返回值就是d4!
其返回值应该是一个数据,且this指针可以在函数内调用,这也是this指针的一大应用!
但是这里的返回类型是对象,因此会调用拷贝构造函数!(每一次重载赋值都要调用一次拷贝构造函数,效率太低下!)
出了作用域,this指针不在,但是*this还存在!因此这里我们建议使用引用返回!(返回的是*this的别名)
因此,最终我们的赋值运算重载函数就从上面进化到了下面:
注意点:所有的指针类型都是内置类型,哪怕是stack* / queue*!
对于下面的代码:
d1 = d1;
此时对于上面的函数体内,this和d实际上都是d1的地址,因此此时赋值没有意义,我们可以将其做以下的修改:(如果相等直接返回任意一个的地址即可,不需要中间赋值,从而提高效率)
Date operator=(const Date& x){if (this != &x)// if(*this != x){_year = x._year;_month = x._month;_day = x._day;}return *this;}
如果将if替换为下面的注释行,即比较两个对象是否相等,此时需要调用赋值运算符重载,但是我们实现函数就是为了完成赋值运算符重载!发生报错!
注意:
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:
内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
默认生成的赋值重载跟拷贝构造行为一样!
- 内置类型成员 -- 值拷贝/浅拷贝;
- 自定义类型成员会调用它的赋值重载!
能不能将赋值运算符重载写成一个全局函数?
不能!因为他是默认的成员函数(类中),如果写在外面会与类中的发生冲突! --> 默认成员函数不可以写在全局域中!但是可以类和声明分开写(类中声明,外面定义)!
三、时间类的功能实现
1、获取一个月有多少天的功能实现:
int GeiMonthDay(int year, int month){int daysArr[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 daysArr[month];}}
函数的优化:
第一个优化:
采用下面这种代码形式避免了每次都有先判断是不是闰年,判断闰年的代码较长,先判断闰年效率低下;
第二个优化:
因为该函数在之后需要被频繁调用,直接将开辟好的数组放到静态区有助于避免每次调用都要开辟栈帧,从而提高效率!
搞成内联函数的话也可以,但是编译器不一定会将其变为内联;
引用返回也可以,但是每次返回都是一个整型,只有四个字节,没有必要必须引用返回!且因为返回了整数常量,引用前还需要加上const!太过于麻烦!
一般内置类型传值传参不需要引用!
调整后的函数如下:
int Date::GetMonthDay(int year, int month)
{static int daysArr[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)){return 29;}else{return daysArr[month];}
}
2、日期 + 天数
我们给出一个初级的函数代码:
Date Date::operator+(int day)
{_day += day;while (_day > GetMonthDay(_year,_month)){_day -= GetMonthDay(_year, _month);++_month;while (_month == 13){++_year;_month = 1;}}return *this;
}
但是这个函数存在一些问题!
对一个变量来说,例如:
i+100;
i的值没有发生改变,实际上我们如果传入一个时间,要计算当日之后的100天是几月几号,实际上计算的是+=!(返回的date是+过日期后的date,date的值发生改变)(返回的是一个对象,因此我们这里最好用引用!)
对于内置类型来说,连续的+=可是满足语法条件的!
接下来我们尝试使用+实现:(+不能改变自己)
尽管引用返回会调用拷贝构造函数,降低效率,但是tmp出作用域被销毁,因此必须使用传值返回!(实现+=的时候*this没有被销毁,因此可以返回引用!)
整个表达式的返回值为tmp!
实现了+=之后,再实现+可以通过复用!
同样实现了+之后,再实现+=可以通过复用!
但是两种方法的对象的调用情况不一样:
这里的+=没有创建对象,运用的是引用;然后+创建了一次,返回了一次,共调用两次;
这种情况完成了4次的对象的创建,+中创建tmp再返回tmp(2次);
+=中调用+也创建了两次,一共4次! (调用拷贝构造函数!)(*this不用创建)
3、前置++和后置++的重载
日期类也支持++的操作!
且++是一个单运算符,只有一个操作数!
前置++和后置++的区别:
- 前置++返回++后的对象;
- 后置++返回++前的对象;
因此不能用一个函数运算符重载!
class默认的operator++就是前置++,实现代码如下:
Date Date::operator++()
{*this += 1;return *this;
}
两个重载的运算符能不能构成函数重载?
可以!依然是根据参数不同调用不同的函数,后置++就是通过函数重载实现的!
规定后置++的参数列表中+上一个int类型用来区分!
可以不加上形参,因为这个参数不是用来接受传递值的,仅仅是为了占位,只是用来区分前置++和后置++的,构成函数重载!
当执行++d1的时候,编译器会自动转化为d1.operator() ;
当执行d1++的时候,编译器会自动转化为d1.operator(0) ;
(用来区分)
为什么说前置的效率比后置好?
后置会调用拷贝构造创建tmp,在返回对象tmp时也会调用,而前置++不会调用;
因此前置的效率比后置的高!
总的实现代码如下所示:
Date.h
#pragma once
#include<iostream>
using namespace std;
class Date
{
public:Date(int year = 1, int month = 1, int day = 1);void Print(){cout << _year << "_" << _month << "_" << _day << endl;}bool operator<(const Date& x);bool operator==(const Date& x);bool operator<=(const Date& x);bool operator>(const Date& x);bool operator>=(const Date& x);bool operator!=(const Date& x);// 日期 + 时间的返回值还是一个日期// 实现获取当前月份的天数int GetMonthDay(int year, int month);// 实现 日期 + 时间 返回日期Date& operator+=(int day);Date operator+(int day);Date operator++(); // 前置++Date operator++(int); // 后置++private:int _year;int _month;int _day;
};
Date.cpp
#define _CRT_SECURE_NO_WARNINGS
#include"data.h"// 分开写拷贝构造函数
Date::Date(int year, int month, int day)
{_year = year;_month = month;_day = day;
}
bool Date::operator<(const Date& 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;
}bool Date::operator==(const Date& x)
{return _year == x._year&& _month == x._month&& _day == x._day;
}// d1<d2
// this = d1
// x = d2
bool Date::operator<=(const Date& x)
{return *this < x || *this == x;
}// 大于就是小于等于取反!
bool Date::operator>(const Date& x)
{return !(*this <= x);
}bool Date::operator>=(const Date& x)
{return !(*this < x);
}bool Date::operator!=(const Date& x)
{return !(*this == x);
}
int Date::GetMonthDay(int year, int month)
{static int daysArr[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)){return 29;}else{return daysArr[month];}
}
Date& Date::operator+=(int day)
{// 方法一_day += day;while (_day > GetMonthDay(_year,_month)){_day -= GetMonthDay(_year, _month);++_month;while (_month == 13){++_year;_month = 1;}}return *this;// 方法二// *this = *this + day;// return *this
}Date Date::operator+(int day)
{// 通过拷贝构造创建一个tmpDate tmp(*this);// 方法二:复用+=tmp += day; // 方法一//tmp._day += day;//while (tmp._day > GetMonthDay(tmp._year, tmp._month))//{// tmp._day -= GetMonthDay(tmp._year, tmp._month);// ++tmp._month;// while (tmp._month == 13)// {// ++tmp._year;// tmp._month = 1;// }//}return tmp;
}
Date Date::operator++() // 前置++
{*this += 1;return *this;
}Date Date::operator++(int) // 后置++
{Date tmp = Date(*this);*this + -1;return tmp;
}
main.cpp
#define _CRT_SECURE_NO_WARNINGS
// 运算符重载有一个自定义类型即可
// 不用全部类型都是自定义类型,例如:日期(自定义类西) + 天数(int)
#include"data.h"
void test1()
{Date d1(2023, 11, 20);d1 + 100;d1.Print();//Date d2(d1+100);Date d2 = d1 + 100; // 上面两种调用方法都可以!d2.Print();d1 += 200;d1.Print();}
void test2()
{Date d1(2023, 11, 20);++d1;d1++;
}
int main()
{test1();return 0;
}