赞
踩
目录
(文章末尾有完整代码)
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 第二个会作为参数。
在==中体现不多,但是在< > -中就有区别了。
赋值重载,即赋值拷贝,也就是将一个已经存在的对象赋值给另一个已经存在的对象。
两个对象都是已经存在的!因此不要与拷贝构造混淆。
- //拷贝构造
- Date d3=d1;
- //赋值重载
- Date d4(1999,11,9);
- d1=d4;
同上写法:
进一步的,为了支持连续的赋值:
d1=d2=d3;
我们也可以在类中将赋值运算符写为:
Date operator=(const Date& d);
给重载的赋值运算符一个返回值,就像原本的整数间的赋值运算符一样。
为了更详细的学习、了解赋值运算符的重载,我们先学习一下引用作为返回参数的一些知识。
传值返回会生成当前对象的拷贝,会拷贝一个临时对象来作为函数的返回值
- Date func(){
- Date d(2024,4,14);
- return d;
- }
而传引用返回不会去拷贝,但是这样的写法是不正确的。因为出函数的时候d就已经被销毁了
- Date& func(){
- Date d(2024,4,14);
- return d;
- }
当返回对象作为一个局部对象或临时对象,出了函数作用域就会调用析构函数,因此也不能直接这样使用引用返回。 因此,传值返回虽然多拷贝一次,但是能正确返回。
可以用静态区变量解决这个问题。
在函数返回值的接受处也应当注意(用静态区开辟后引用返回时):
对于这两种接受办法,右侧的ref在接受时还会再拷贝一次(将返回值d的值拷贝进ref),左侧不会再拷贝。
但是如果使用传值返回,还要注意接收处的权限问题,需要加const来适应临时拷贝所具有的常性。
tips:所有的传值返回,传回的值都会拷贝给临时变量,而临时变量具有常性,因此大部分的传值返回都要注意临时变量的常性带来的权限问题。
▲分析四种经典的与引用有关的返回:
1. Date返回,Date接受:
最纯粹、最简单的返回:用值返回,开新空间接受值,也是曾经各位同学用的最多的返回模式。其本质为:函数func返回的是d的临时拷贝,临时变量(临时拷贝)是存放在当前函数栈帧的,也就是说d的拷贝是存放在main函数中的。func返回了d的拷贝之后,ref1开辟出一个新的空间,在该新空间中拿到该临时变量的值(也就是将临时变量又拷贝到了ref1的新空间中去),因此,使用这样的用值返回、用值接受时,没有权限问题,func接受到的也是一组独立的完整的数据,不用担心销毁、覆盖一类的问题。
2.Date返回,Date&接受:
![]()
func返回的是d的临时拷贝,临时拷贝存储在main的函数栈帧中,并且具有常性,ref1直接作为该临时拷贝的别名,能修改和阅读该临时拷贝,属于权限的放大,因此报错。
加上一个const,就不会报错了:
因为ref是临时拷贝的别名,临时拷贝的生命周期与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中的数据也很安全,不会被覆盖。
总而言之:
当然,引用返回在很多场合下可以减少拷贝,效率更高。
只要引用对象生命周期没有结束,就应该使用引用返回
回到刚才的赋值运算符重载的问题:
在类中实现赋值运算符重载时,就可以返回(*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;
- }
这样就更加完美了。
既然作为类中的默认成员函数,当我们没有显式实现运算符重载时,编译器会自动生成,性质与默认拷贝类似。需要深拷贝的,依然需要我们自主实现,编译器生成的只能实现浅拷贝
最后:赋值运算符重载不能实现在全局
提问:拷贝构造中也有“=”的写法,赋值运算符中也有“=”的写法,是不是实现一个就可以了呢?
欢迎留言评论区。
拷贝构造中的“=”和赋值运算符中的“=”本质上没有关系。例如:
string s1 = "123"; string s2 = s1; // 调用拷贝构造 string s3; s3 = s1; // 调用赋值重载string s2=s1其实是和string s2(s1)是一样的,只是换了一种书写方式,恰好使用了和赋值运算符一样的符号“=”而已
我们以日期为例讲解了类和对象中的各种知识,现在我们来真正实现这个类
- bool Date :: operator==(const Date& d) {
- return _year == d._year &&
- _month == d._month &&
- _day == d._day;
- }
我们选择将函数的实现和声明分离(在Date.cpp文件中实现,在Date.h文件中声明)
直接将函数定义在类中,默认其为inline函数;如上分开实现,则不会默认其为inline函数。
- 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
在实现了相等、大于、小于之后,当然可以通过直接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解引用来获取该对象
之前我们说到,只要有一个操作数是自定义类型,就可以实现运算符重载
除了双目操作数,还有日期类与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,程序将出错。所以应当在两个被复用(+= -=)重载中加上判断。毕竟,重载的目的是增强代码可读性。
多个同一运算符重载可以构成函数重载,如下图(相同的运算符,不同的运算符参数,同样的函数名,不同的参数构成函数重载)
以上两种-尚且存在不同的参数,若参数名等全部相同呢?
- //单目操作数
- //++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;
- }
(这个不是单目操作符,但是有了前文才能足够好的理解)
在之前(包括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;
- }
将 const 修饰的 “ 成员函数 ” 称之为 const 成员函数 , const 修饰类成员函数,实际修饰该成员函数 隐含的 this 指针 ,表明在该成员函数中 不能对类的任何成员进行修改。
将一个只读的数据传给一个参数可读可写的函数,也算是权限的放大
d1是const Date ,因此d1传给Print的实参是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修饰过的变量进行操作,我们可以给大部分的函数加上一个后缀的const(声明和定义处都要加),这样既能操作如上图的d1,也能操作如上图的d2(缩小函数的权限,让低权限和高权限的变量都能被使用).
当然,也不是所有的都适合使用后缀const:
比如+= -=的重载就不能在函数名之后加const。 因为+=或则-=是需要对其对应的this作出修改的
答案为:不可以、可以、不可以、可以
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。意义不大,但是可以用来返回假地址,返回你希望对方得到的地址。可以用来干坏事。
- //.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;
- }

Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。