【C++】运算符重载(日期类的实现)

文章目录

  • 前言
  • 一、运算符重载的概念和意义
  • 二、运算符重载的规则
  • 三、常用运算符重载
    • 1.关系运算符重载
    • 2.=赋值运算符重载
    • 3.+=、-=、+、-重载
    • 4.前置++和后置++重载
    • 5.流插入<<和流提取>>重载

前言

之前在总结类的六个默认成员函数时,没有过多介绍运算符重载,只简单介绍了赋值运算符重载。本节内容将会总结常用的运算符重载,以实现一个日期类为例。

一、运算符重载的概念和意义

什么是运算符重载?

运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据时做出不同的行为。

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。

运算符重载的语法格式

返回值类型 operator运算符(参数列表)
{
	函数体
}

:这里的运算符可以是+、-、*、/、>、>=等,但不能创建新的运算符如@、$等。
.* :: sizeof ?: .这5个运算符不支持重载。

二、运算符重载的规则

以下面一个日期类为例

class Date
{
public:
	Date(int year = 1, 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)
{
	return d1._year == d2._year && d1._month == d2._month && d1._day == d2._day;
}

判断是否相等的运算符==是双目运算符,所以参数列表有两个参数。因为传值传参会调用拷贝构造,降低效率,所以用传引用传参;
因为不改变实参,所以参数最好加上const;成员函数后面加const,对于不改变成员变量的成员函数,我们最好在函数名后面加上const,提高代码健壮性。

但是我们如果重载成全局函数,就需要类成员变量是公有属性,因为类外无法访问类的私有成员。但是这样就会破坏封装性。 因此,为了保证封装性,我们一般将运算符重载为类的友元函数或类的成员函数

  • 重载为类的友元函数(全局函数)
friend bool operator==(const Date& d1, const Date& d2);

函数定义不变,只需要在类中加上友元的声明,就可以正常使用上述函数,还不会破坏类的封装性。

  • 重载为类的成员函数
bool operator==(const Date& d) const
{
	return _year == d._year && _month == d._month && _day == d._day;
	//等价于
	//return this->_year == d._year && this->_month == d._month && this->_day == d._day;
}

可以看到,参数个数减少了一个,这是为什么呢?
答:这是由于类的每个非静态成员函数都有一个隐藏的this指针,占第一个参数的位置,也就是说,上述写法表面上是一个参数,实际上有两个参数。如下,但我们定义时不需要显示地传this指针。

//等价于,但不能这样写
bool operator==(Date* this, const Date& d2) const

注:对于不改变this的成员函数,我们最好在函数名后面加上const,变成常成员函数,增加代码健壮性。

总结:作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针

运算符重载的调用方法如下,和之前调用方法一样。

int main()
{
	Date d1(2024, 6, 25);
	Date d2(2024, 6, 24);
	//d1 == d2
	//重载为友元函数,等价于if(operator==(d1, d2))
	//重载为成员函数,等价于if(d1.operator==(d2))
	if (d1 == d2) 
	{
		cout << "d1 == d2" << endl;
	}
	return 0;
}

运算符重载规则:

1.只能对已有的运算符进行重载,不可以创建新的运算符
2.重载后的运算符的优先级、结合性也应该保持不变,也不能改变其操作个数和语法结构。
3.用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
4.作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针。
5..* :: sizeof ?: . 这5个运算符不支持重载。
6.赋值、下标[]、调用()、成员访问->这4个运算符必须重载为类的成员函数,不能重载为全局函数。
4.若一个运算符的操作需要修改对象的状态(修改this指针),最好重载为类的成员函数。

三、常用运算符重载

1.关系运算符重载

总共有==、!=、<、<=、>、>=这六个关系运算符,进行对象之间的比较判断,所以返回值为false或者true,即bool类型。

这里我将关系运算符重载为类的成员函数(也可以重载为类的友元函数)

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	//函数重载声明
	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;
private:
	int _year;
	int _month;
	int _day;
};
//定义
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)
	{
		if (_month == d._month)
			return _day < d._day;
		else
			return _month < d._month;
	}
	else
	{
		return _year < d._year;
	}
}

bool Date::operator<=(const Date& d) const
{
	return *this < d || *this == d;
}

bool Date::operator>(const Date& d) const
{
	return !(*this <= d);
}

bool Date::operator>=(const Date& d) const
{
	return !(*this < d);
}

重载之后我们就可以比较类类型对象的大小。要理解关系运算符的对应关系,比如实现了=<重载,我们可以借此来更简单地实现<=>等。

2.=赋值运算符重载

前面总结类的六个默认成员函数时,已经介绍过赋值运算符重载。赋值运算符重载只能重载为类的成员函数。
赋值运算符重载用在两个及以上已存在的对象之间进行赋值。分清这点与拷贝构造(用已存在对象初始化新对象)的区别。

赋值运算符重载格式:

参数类型:const T&,传递引用可以提高传参效率;
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值;
返回*this :支持连续赋值;
检测是否自己给自己赋值;

日期类的赋值运算符重载(可以不用写)

Date& operator=(const Date& d)//传引用提高传参效率
{
	if (&d != this)//判断优化
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
}

赋值运算符重载也是类的默认成员函数,我们不写,编译器会生成一个默认赋值运算符重载,完成数据的浅拷贝。

对于日期类这种成员变量都是内置类型的类,我们不需要显示定义赋值运算符重载,使用编译器默认生成的就可以了。但是涉及到资源申请的比如栈这种,浅拷贝就无法满足,需要我们自己显示定义赋值运算符重载完成深拷贝。

3.+=、-=、+、-重载

对于+=-=运算符,我们知道,这两个运算符会改变对象自身,且返回修改后的对象。

+= 的第二个操作数有可能是负数,-=的第二个操作数有可能是正数,因此先进行判断,情况要考虑周全。

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 > 12)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;//返回对象本身
}

Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}
	_day -= day;
	while (_day <= 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 day) const
{
	Date tmp(*this);
	tmp -= 1;
	return tmp;//出了作用域销毁,不能用引用返回
}

上述函数重载都是对日期进行指定天数的加减运算,那如何进行日期与日期间的运算呢?运算符重载不能实现无意义的重载,比如两个日期相加,是不符合逻辑的;但两个日期是可以相减的,返回两个日期的差值。

int Date::operator-(const Date& d) const
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int cnt = 0;
	while (max != min)
	{
		cnt++;
		min++;
	}
	return cnt * flag;
}

这里的减-与之前的减-构成重载关系。

注意:

1.+=-=会改变对象本身,且返回*this即对象本身,所以可以用引用返回提高效率。
2.+-并不会改变对象的值,返回的是对象进行加减运算后的值(临时变量),不能用引用返回。

4.前置++和后置++重载

前置++(- -)和后置++(- -)都是单目运算符,如果重载为类的成员函数,则是无参的。

为了让前置++与后置++能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递。 也就是说,二者的调用还是和内置类型调用的方法一样,只不过定义重载时后置++的参数多个无意义的int参数。

由于前面已经实现了+=的功能,这里不再重复了,直接套用即可。

//前置++
Date& Date::operator++()
{
	*this += 1;
	return *this;
}
//后置++
Date Date::operator++(int)//int无任何意义,只是为了区分前置还是后置
{
	Date tmp(*this);
	*this += 1;
	return tmp;
}

调用方法

int main()
{
	Date d1(2024, 6, 25);
	++d1;//等价于d1.operator++();
	d1++;//等价于d1.operator++(0);
	return 0;
}

调用后置形式的重载函数时,对于那个没用的 int 形参,编译器自动以 0 作为实参。

前置++: 返回+1之后的结果,this指向的对象再函数结束后不会销毁,所以用引用返回提高效率。
后置++: 先使用后+1,因此需要返回+1之前的旧值,在实现时需要先将原始对象保存一份,然后*this+1;注意临时对象只能以值的方式返回,不能返回引用。

前置- -与后置- -也是同理,这里不再具体实现。

5.流插入<<和流提取>>重载

C++标准库对左移运算符<<和右移运算符>>分别进行了重载,与cout和cin搭配使用,使其能够用于不同数据的输入输出。

int i = 0;
double d = 1.23;
cout << i;
cout << d;

<<和>>可以直接支持内置类型是因为C++标准库里已经实现好了,我们可以直接使用;
可以直接支持自动类型识别是因为函数重载。

对于内置类型,我们可以直接使用,但对于自定义类型,我们需要重载这两个操作符。

比如对于日期类,我们重载这两个运算符后,可以实现输入和输出年月日。

Date d1;
cin >> d1;
cout << d1;

通过查阅C++官网资料,我们知道,cin是istrem类的对象,cout是是ostrem类的对象,这两个都在<iostream>头文件中;因为C++标准库中istrem类和ostrem类重载了内置类型的参数,所以内置类型可以直接使用并且可以自动识别类型。

在这里插入图片描述

在这里插入图片描述

为什么输入输出操作符要重载为友元函数,不能重载为成员函数?

我们知道,成员函数的第一个参数是隐藏的this指针,如果我们重载为成员函数,也就是说,this指针为左操作数,cout/cin为右操作数,那么就不符合我们常规的调用顺序,不符合使用习惯。

ostream& operator<<(ostream& out)
{
	out << _year << "-" << _month << "-" << _day << endl;
	return out;
}
//调用
int main()
{
	Date d1, d2;
	//与我们常规调用顺序相反cout << d1;
	d1 << cout;//d1.等价于operator<<(cout)
	return 0;
}

实际使用中cin/cout为第一个形参对象,才符合常规使用。所以要将这两个运算符重载成全局函数。但又会导致类外没办法访问非公有成员,就需要借助友元来解决。

class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	Date(int year = 1, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day << "-";
	return out;
}
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}
int main()
{
	Date d1, d2;
	cin >> d1 >> d2;//等价于operator>>(operator>>(cin, d1), d2);
	cout << d1;//等价于operator<<(cout, d1);
	return 0;
}

为什么要有返回值? 为什么返回第一个参数的引用?
答:返回isteam/ostream类对象的引用作为下次调用时的左操作数,是为了能够连续读取。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/748015.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

实时显示用户输入PySide6实例

如何用 PySide6 实现QLabel 实时显示用户在 QLineEdit 内输入的内容&#xff1f; 示例代码&#xff1a; # QLineEdit 用户输入内容&#xff0c;QLabel 即时显示用户输入训练from PySide6.QtWidgets import (QApplication, QWidget,QLabel, QLineEdit, QVBoxLayout)class MyWi…

ROS学习记录:Hector_Mapping建图的参数设置

前言 launch文件启动Hector_Mapping的建图功能 在上一篇文章&#xff08;以上链接&#xff09;通过launch文件启动了Hector_Mapping建图功能&#xff0c;这一篇文章将在launch文件里给Hector_Mapping设置参数 一、Hector_Mapping有哪些参数 1、浏览器搜索并进入 ROS index 2…

Redis-实战篇-编码解决商铺查询的缓存穿透问题(缓存空对象)

文章目录 1、缓存穿透2、常见的解决方案有两种&#xff1a;2.1、缓存空对象2.2、布隆过滤器 3、编码解决商铺查询的缓存穿透问题3.1、queryById3.2、RedisConstants.java 1、缓存穿透 缓存击穿是指客户端请求的数据在缓存中和数据库中都不存在&#xff0c;这样缓存永远不会生效…

首次30米空间分辨率生成中国年度耕地栅格数据1986-2021

中国1986-2021年30米分辨率年度耕地数据集 数据介绍 精确、详细且及时的耕地范围信息对于粮食安全保障和环境可持续性至关重要。然而&#xff0c;由于农业景观的复杂性和足够训练样本的缺乏&#xff0c;在大范围下进行高时空分辨率的耕地动态监测仍然具有挑战性&#xff0c;尤其…

1.Android逆向协议-环境搭建

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a;易锦网校 不是安卓逆向吗&#xff1f;为什么写java代码&#xff1f;因为逆向的时候涉及java语言 JDK环境搭建&#xff1a;JDK是JAVA语…

基于RK3568车载电脑助力日本巴士公司高效完成巴士到站系统项目部署

无处不在的物联网&#xff08;IoT&#xff09;技术已经渗透到了人类生活的各个角落&#xff0c;如日常出行乘坐的公交车上&#xff0c;物联网&#xff08;IoT&#xff09;技术的应用就得到完美诠释&#xff01;其通过公交车上的车载电脑网络与中控室服务器连接来对公交车的运行…

Jira实践案例分享:小米集团如何通过API请求优化、数据治理与AI智能客服等,实现Jira系统的高效运维

日前&#xff0c;Atlassian中国合作伙伴企业日活动在上海成功举办。活动以“AI协同 创未来——如何利用人工智能提升团队协作&#xff0c;加速产品交付”为主题&#xff0c;深入探讨了AI技术在团队协作与产品交付中的创新应用与实践&#xff0c;吸引了众多业内专家、企业客户及…

自定义注解实现幂等

在前面的文章中&#xff0c;我们说过解决消息重复消费的方式中&#xff0c;有一个方式是幂等&#xff0c;那么幂等是怎么实现呢&#xff1f; 面试官&#xff1a;对于MQ中的消息重复消费说说的你的理解 一、定义 首先我们先来了解一下幂等的定义&#xff0c;它指的是同一个操作…

嵌入式Linux系统编程 — 3.7 文件目录与处理

目录 1 文件目录 1.1 文件目录简介 1.2 目录存储形式 2 创建和删除目录 2.1 mkdir创建目录 2.2 rmdir删除空目录 3 opendir打开、 readdir读取以及closedir关闭目录 3.1 打开文件 opendir 3.2 读取目录 readdir 3.3 重置目录起点rewinddir 3.4 关闭目录 closedir 3…

基于DeepNLP AI Store真人点评和ShowCase分享社区-AI for Image Generator

来源 quora 社区: https://deepnlpaistore.quora.com/ github: https://rockingdingo.github.io/deepnlp/store/image_generator 内容 DeepNLP AI Store 网址&#xff1a;http://www.deepnlp.org/store/image-generator 网站针对图像生成类别 Image Generator下多个AI工具如 …

第 28 篇 : SSH秘钥登录

1 生成秘钥 ssh-keygen -t rsa ls -a ./.ssh/一直回车就行了 2. 修改配置 vi /etc/ssh/sshd_config放开注释 公钥的位置修改 关闭密码登录 PubkeyAuthentication yes AuthorizedKeysFile .ssh/id_rsa.pub PasswordAuthentication no3. 下载id_rsa私钥, 自行解决 注意…

Websocket在Java中的实践——自动注册端点

在《Websocket在Java中的实践——握手拦截器》中我们使用握手拦截器实现了路径解析的工作。这个过程略显复杂&#xff0c;因为路径解析这样比较底层的工作应该由框架来解决&#xff0c;而不应该交由开发者来做。本文介绍的自动注册端点的功能就可以很优雅的解决这个问题。 依赖…

GNU、Unix、Linux、Makefile、GCC、GDB、GPL、CentOS 7、Ubuntu之间的关系

全文总结 早期&#xff0c;Unix系统作为一类强大的操作系统&#xff0c;在计算领域奠定了基础。然而&#xff0c;出于对软件自由的追求&#xff0c;Richard Stallman在1983年发起了GNU项目&#xff0c;旨在创建一个完全自由的、与Unix兼容的操作系统。GNU项目不仅倡议软件自由…

初创企业合规管理中的企业合规义务边界问题

在初创企业的迅猛发展过程中&#xff0c;合规管理是确保公司可持续成长和避免潜在风险的关键因素。而在合规管理中&#xff0c;界定企业边界尤为重要&#xff0c;它关系到企业如何合理规划业务范围、管理内部外部关系以及维护企业形象和法律责任的清晰。 一、初创企业面临的合…

ubuntu 18 虚拟机安装(3)安装mysql

ubuntu 18 虚拟机安装&#xff08;3&#xff09;安装mysql 参考 https://cloud.tencent.com/developer/article/1700780 技术分享 | MySQL 设置管理员密码无法生效一例 https://cloud.tencent.com/developer/article/2014384 在Ubuntu18.04上安装MySQL &#xff5c; 超级详细…

flink-触发器Trigger和移除器Evictor

Trigger 触发器 触发器作用&#xff1a;控制窗口什么时候除法计算。即执行窗口函数&#xff1b;基于WindowStream调用trigger&#xff08;&#xff09;方法&#xff0c;传入自定义触发器&#xff08;trigger&#xff09;&#xff1b; 每一个窗口分配器&#xff08;windowAssi…

如何以智能方式安装 Python

Python易于使用&#xff0c;对初学者友好&#xff0c;功能强大&#xff0c;几乎可以为任何应用程序创建强大的软件。 但与任何其他软件一样&#xff0c;Python 的设置和管理可能很复杂。 在本文中&#xff0c;我们将介绍如何正确设置 Python。 您将学习如何选择合适的版本、…

JavaScript中常用数据类型做布尔值(Boolean)转换

一、前言 二、示例 1、String转Boolean 2、Number转Boolean 3、NaN、Null、undefined 转Boolean 4、Object转Boolean 5、Array转Boolean 6、Symbol转Boolean 三、总结 四、思考 一、前言 JavaScript中&#xff0c;经常需要对一些值进行boolean判断&#xff0c;根据判…

three.js 第六节 - 纹理以及贴图【.hdr文件(hdr贴图)】- 色彩空间

素材 这是素材 更多素材、案例、项目 好几个G一共&#xff0c;加我q178373168&#xff0c;60大洋拿走 源码 源码 // ts-nocheck // 引入three.js import * as THREE from three // 导入轨道控制器 import { OrbitControls } from three/examples/jsm/controls/OrbitControls…

【考研数学】李林《880》25版主要变化汇总

25版李林880拿到手后对比&#xff0c;发现和24几乎没有太大差别&#xff08;高数前两章20多道题目增删&#xff09;&#xff0c;然后24又和23版本一模一样 所以有24版本or23版本的880都可以&#xff0c;不用一定追求25年的&#xff01; 880算是比较经典的习题了&#xff0c;搭…