C++类和对象进阶:运算符重载深度详解
C++类和对象进阶:运算符重载
- 前言
- 引入
- 运算符重载
- 定义
- 语法
- 注意事项
- 重载为全局函数
- 重载为成员函数
- 运算符重载的本质
- 默认赋值运算符重载(默认成员函数)
- 编译器自己生成的赋值运算符重载函数
- 需要自己实现的场景
- 总结默认赋值运算符重载
- 拷贝构造函数和赋值重载的区分
- 验证
- 总结
前言
在C++中,运算符重载允许我们为自定义类型赋予与内置类型相似的操作方式,极大提升了代码的可读性和灵活性。本文将深入探讨运算符重载的规则与实现,并重点分析默认成员函数之一的赋值运算符重载函数。
引入
class Date {
public:Date(int year = 2025, int month = 2, int day = 22) {this->_year = year;this->_month = month;this->_day = day;}
private:int _year;int _month;int _day;
};
int main(){Date d1, d2;//d1 == d2; // 若无运算符重载,这样的写法未定义。//if(d1 > d2){;} // 若无运算符重载,这样的写法未定义。
}
思考以下场景:
如果想
- 1. 比较两个日期类对象是否相等
- 2. 两个日期相减的运算来计算相差的天数
- 3. 计算一个日期100天后是什么日期
C++为了满足自定义类型中以上类似需求并为了增强代码的可读性,引入了运算符重载。
运算符重载
定义
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
。
语法
函数名字为:关键字operator后面接需要重载的运算符符号。
例如要对==
进行重载
//假定返回值为bool
bool operator==(int x, int y); //声明
函数原型:返回值类型 operator操作符(参数列表)
注意事项
- 不能通过连接其他符号来创建新的操作符:比如operator@。
- 重载操作符必须有一个类类型参数。
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义。
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针。
.*
,::
,sizeof
,? :
,.
注意以上5个运算符不能重载。
总结以上内容:
- 函数格式:
返回类型 operator运算符(参数列表)
- 关键限制:
- 只能重载C++中已有的运算符,不能创造新的运算符。
- 重载运算符主要是针对自定义类型的,因此
operator
必须有一个类类型参数. - 不能改变运算符对内置类型操作的原始含义。
- 要重载的运算符有几个操作数,operator中就有几个参数(算上this指针)
- 以下运算符不可重载:
.*
::
sizeof
?:
.
。
重载为全局函数
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;
}
class Date {
public:Date(int year = 2025, int month = 2, int day = 22) {this->_year = year;this->_month = month;this->_day = day;}
//private: //暂时设为public,是为了让全局重载的<可以访问到类内的成员变量
public:int _year;int _month;int _day;
};
int main(){Date d1(2025, 2, 12);Date d2(2024, 2, 12);cout << (d2 < d1) << endl;//d2 < d1 会被编译器转换成 d2.operator(d1),本质上是调用函数return 0;
}
注意:d1 < d2
, 重载后的 <
,Date
类对象使用<
号时,左操作数是这里的d1
,右操作数是这里的d2
这里会发现运算符重载成全局函数,需要成员变量是公有的,那么问题来了,封装性如何保证?
为解决这一问题,我们可以重载成成员函数。
重载为成员函数
//运算符重载
class Date {
public:Date(int year = 2025, int month = 2, int day = 22) {this->_year = year;this->_month = month;this->_day = day;}//操作符是几个操作数,operator函数就有几个参数(应该包括上隐含的this指针参数)//也可以全局重载,但在类内重载更方便,可以直接访问私有成员bool operator<(const Date& d) { //自定义类型,最好传引用,类内不能通过形参d修改原变量,加上constif (this->_year < d._year)return true;else if (this->_year == d._year && this->_month < d._month)return true;else if (this->_year == d._year && this->_month == d._month && this->_day < d._day)return true;elsereturn false;}
private:int _year;int _month;int _day;
};
bool operator<(const Date& d);
,这里需要注意的是,由于第一个形参是this, 左操作数是*this,是调用函数的对象,右操作数是传入的另一个对象。- 是否需要重载运算符,要看这些运算符对该类型是否有意义
对比分析:
特性 | 成员函数形式 | 全局函数形式 |
---|---|---|
访问权限 | 可直接访问私有成员 | 需友元声明 |
左操作数类型 | 必须是类对象 | 任意类型 |
隐式this参数 | 有 | 无 |
对称性操作符 | 不便于处理 | 更适合(如<<流操作符) |
掌握以上规则,我们便学会了如何对运算符进行重载。
接下来来看六大默认成员函数中的:赋值运算符重载。
运算符重载的本质
由上图汇编代码可以看到:
d1 < d2
d1.operator(d2)
本质都是调用了类内的函数。也正因如此,运算符重载函数也可以按照函数重载的规则重载。
默认赋值运算符重载(默认成员函数)
我们早已知道,赋值运算符重载是类内的一个默认成员函数。
C语言中自定义类型可以完成赋值操作(例如:同类型结构体之间的赋值),C++中的
class
同样支持赋值操作,只不过C++对这一行为进行了优化与升级。
- 赋值运算符重载格式
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值,允许自己给自己进行赋值,但此时不需要拷贝,直接返回当前对象本身。
- 返回*this :要复合连续赋值的含义
//运算符重载
class Date {
public:Date(int year = 2025, int month = 2, int day = 22) {this->_year = year;this->_month = month;this->_day = day;}//操作符是几个操作数,operator函数就有几个参数(应该包括上隐含的this指针参数)//也可以全局重载,但在类内重载更方便,可以直接访问私有成员bool operator<(const Date& d) { //自定义类型,最好传引用,类内不能修改,加上constif (this->_year < d._year)return true;else if (this->_year == d._year && this->_month < d._month)return true;else if (this->_year == d._year && this->_month == d._month && this->_day < d._day)return true;elsereturn false;}//赋值运算符重载是默认成员函数,编译器会自己生成, 不能写成全局的,Date& operator=(const Date& d) {//if (*this != d) // 这样子比较,代价有点大,将 != 重载后是可以实现的,但是没必要if (this != &d) { //防止这样的赋值 d1 = d1 如果自己给自己赋值,可以不复制this->_year = d._year;this->_month = d._month;this->_day = d._day;}return *this; //返回对象的别名,出了作用域,*this生命周期还在}
private:int _year;int _month;int _day;
};
注意:赋值运算符只能重载成类的成员函数不能重载成全局函数
。
原因:
- 赋值运算符重载成全局函数时,就没有this指针了,无法访问类内的私有变量,需要给两个参数,如下示例:
Date& operator=(Date& left, const Date& right){if (&left != &right){left._year = right._year;left._month = right._month;left._day = right._day;}return left;}
- 赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。
编译器自己生成的赋值运算符重载函数
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
- 观察以下程序
class Stack{
typedef int DataType;
public:Stack(size_t capacity = 10){_array = (DataType*)malloc(capacity * sizeof(DataType));if (nullptr == _array){perror("malloc申请空间失败");return;}_size = 0;_capacity = capacity;}void Push(const DataType& data){// CheckCapacity();_array[_size] = data;_size++;}~Stack(){if (_array){free(_array);_array = nullptr;_capacity = 0;_size = 0;}}
private:DataType *_array;size_t _size;size_t _capacity;
};
int main(){Stack s1;s1.Push(1);s1.Push(2);Stack s2;s2 = s1;return 0;
}
以上程序会报错
报错原因和往期文章中的拷贝构造函数类似。
main
函数结束时,会调用Stack中的析构函数对s1和s2两个对象进行析构。- 编译器默认生成的赋值运算符重载进行的是值拷贝。由于进行了
s2 = s1
的赋值操作,赋值过后, s2和s1中变量_array
存放了同一块空间的地址。 main
函数结束后,会调用st1
和st2
的析构函数,由于地址相同,则会对同一块空间析构两次(对同一块空间free
两次)。因此会报错。
需要自己实现的场景
总结默认赋值运算符重载
- 默认成员函数:默认赋值运算符重载是默认成员函数
- 重载特性:只能重载成类的成员函数不能重载成全局函数,若定义为全局函数会与编译器默认生成的冲突。
- 拷贝方式:编译器生成的默认赋值运算符重载函数完成字节序的值拷贝了(浅拷贝)
- 需要手动实现的场景:如果类中未涉及到资源管理(在堆区申请空间),赋值运算符是否实现都可以;一旦涉及到资源管理则必须要手动实现深拷贝。
拷贝构造函数和赋值重载的区分
思考一下情景,会调用拷贝构造还是赋值重载?
Date d1(2025, 2, 12);
Date d2(2024, 3, 13);
//思考会分别调用什么函数
Date d3 = d1; //拷贝构造 还是 赋值运算符重载 ?
d2 = d1; //拷贝构造 还是 赋值运算符重载 ?
验证
这是我们的测试代码
class Date {
public:Date(int year = 2025, int month = 2, int day = 22){this->_year = year;this->_month = month;this->_day = day;}Date& operator=(const Date& d) {if (this != &d) {this->_year = d._year;this->_month = d._month;this->_day = d._day;}return *this;}Date(const Date& d) {cout << "Date(const Date& d) " << endl;_year = d._year;_month = d._month;_day = d._day;}
private:int _year;int _month;int _day;
};
int main(){Date d1(2025, 2, 12);Date d2(2024, 3, 13);//思考会分别调用什么函数Date d3 = d1; //拷贝构造 还是 赋值运算符重载 ?d2 = d1; //拷贝构造 还是 赋值运算符重载 ?
}
我们来调试验证:
C++拷贝构造与赋值重载的区分
总结:
//区分拷贝构造和赋值运算符重载
Date d1(2025, 2, 12);
Date d2(2024, 3, 13);
//用一个已经存在的对象初始化另一个对象 ----- 调用拷贝构造函数
Date d3 = d1; //等价于 Date d3(d1);//已经存在的两个对象之间赋值拷贝 ----- 赋值运算符重载函数
d2 = d1;
- 用一个
已经存在的对象初始化另一个对象
----- 调用拷贝构造函数 已经存在的两个对象之间赋值拷贝
----- 赋值运算符重载函数
总结
特性 | 运算符重载 | 赋值运算符重载 |
---|---|---|
必要性 | 增强代码可读性 | 资源管理的必要手段 |
默认行为 | 无 | 浅拷贝 |
典型应用场景 | 算术运算、比较运算 | 对象复制、资源管理 |
实现要点 | 操作数类型、返回值优化 | 深拷贝、自赋值检查 |
最佳实践建议:
- 优先使用成员函数形式进行运算符重载
- 对于资源管理类必须实现深拷贝赋值
- 流操作符(<<, >>)建议采用全局函数+友元形式
- 保持运算符的语义一致性(例如+不应修改操作数)
通过合理使用运算符重载,可以显著提升代码的表达能力,使自定义类型的使用更加直观自然。但需谨记"能力越大责任越大",不当的运算符重载反而会降低代码可维护性。
文章到此结束啦,欢迎各位大佬在评论区讨论交流,如果觉得文章写的不错,还请留下免费的赞和收藏!
相关文章:

C++类和对象进阶:运算符重载深度详解
C类和对象进阶:运算符重载 前言引入运算符重载定义语法注意事项重载为全局函数重载为成员函数运算符重载的本质 默认赋值运算符重载(默认成员函数)编译器自己生成的赋值运算符重载函数需要自己实现的场景总结默认赋值运算符重载 拷贝构造函数和赋值重载的区分验证 总…...

Mybatisplus——Mybatisplus3.5.2版本使用Page分页插件查询,records有数据但是total显示0
目录 一、问题背景 debug 执行Mybatisplus使用Page分页插件查询时,发现 Page 里面的records有数据但是total显示0。 二、问题产生的原因 未配置MybatisPlus的分页插件拦截器导致的或者因mybatis-plus版本3.4或3.5版本导致原先的分页插件paginationInterceptor无法…...

C#(Winform)通过添加AForge添加并使用系统摄像机
先展示效果 AForge介绍 AForge是一个专门为开发者和研究者基于C#框架设计的, 也是NET平台下的开源计算机视觉和人工智能库 它提供了许多常用的图像处理和视频处理算法、机器学习和神经网络模型,并且具有高效、易用、稳定等特点。 AForge主要包括: 计算机视觉与人…...

AI使用场景简单测试
前言 今天来分享下AI的2个实用场景,我这里是使用的博主:小虚竹,搭建的AI服务,用的ChatGPT 4O模型,主要是试了3个场景,服装设计、直播带货话术、检验报告分析。 一、服装设计 对于最后需要的裁片设计上的尺寸…...

Linux 配置 MySQL 定时自动备份到另一台服务器
Linux 配置 MySQL 定时自动备份到另一台服务器 前言1、配置服务器通信1.1:配置过程 2、编写自动备份sh脚本文件3:设置定时自动执行 前言 此方案可使一台服务器上的 MySQL 中的所有数据库每天 0 点自动转储为 .sql 文件,然后将文件同步到另一…...

PostgreSQL 备库的延迟问题
目录标题 1. 查看主备状态计算方式:实际情况:举个例子: 2. 查看历史状态3. 分析日志文件4. 查看数据库层面的复制状态5. 检查活动事务6. 检查系统资源7. 检查网络状况8. 检查复制槽状态9. 检查未提交的两阶段事务 要排查 PostgreSQL 备库的延…...
力扣-二叉树-226 翻转二叉树
思路 利用递归的思路 代码 class Solution { public:TreeNode* invertTree(TreeNode* root) {if(root nullptr){return root;}swap( root->right, root->left);invertTree(root->left);invertTree(root->right);return root;} };...

基于SpringBoot的在线车辆租赁信息管理系统
系统展示 用户前台界面 管理员后台界面 系统背景 随着互联网技术的不断发展和人们生活水平的提高,汽车租赁行业迎来了前所未有的发展机遇。传统的汽车租赁方式往往存在流程繁琐、信息不透明等问题,难以满足现代消费者对于便捷、高效服务的需求。因此&…...
掌握 systemd:Linux 服务管理的核心工具
1. 什么是 systemd? 定义:systemd 是 Linux 系统的初始化系统(init system)和服务管理器,用于替代传统的 SysVinit。核心目标: 加速系统启动(并行化任务)。统一管理服务、日志、挂载…...
【信息系统项目管理师-案例真题】2019下半年案例分析答案和详解
更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 试题一【问题 1】(6 分)【问题 2 】(8 分)【问题 3 】(11 分)试题二【问题 1】(5分)【问题 2】 (14 分)【问题 3 】(6 分)试题三【问题 1】(8 分)【问题 2 】(6 分)【问题 3】 (8 分)【问题 4 …...
C/C++程序的内存是如何开辟的?
💬 欢迎讨论:在阅读过程中有任何疑问,欢迎在评论区留言,我们一起交流学习! 👍 点赞、收藏与分享:如果你觉得这篇文章对你有帮助,记得点赞、收藏,并分享给更多对C语言感兴…...

日志结构化处理:PO对象toString日志转JSON工具
日志结构化处理:PO对象toString日志转JSON工具 1. 解决的问题2. 下载地址 在Java项目中,PO(Plain Old Java Object)对象遍布各个角落,且常常伴随着大量的日志记录需求。传统的做法是通过toString方法直接打印这些对象&…...

python学opencv|读取图像(六十五)使用cv2.boundingRect()函数实现图像轮廓矩形标注
【1】引言 前序学习进程中,已经使用cv2.findContours()函数cv2.drawContours()函数实现图像轮廓识别和标注,这种标注沿着图像的轮廓进行,比较细致。相关文章链接为: python学opencv|读取图像(六十四)使用…...

大疆无人机需要的kml文件如何制作kml导出(大疆KML文件)
大疆无人机需要的轨迹kml文件,是一种专门的格式,这个kml里面只有轨迹点,其它的属性信息都不需要。 BigemapPro提供了专门的大疆格式输出, 软件这里下载 www.bigemap.com 安装后,kml导入如下图: 然后选择…...

ArrayList、LinkedList、HashMap、HashTable、HashSet、TreeSet
集合族谱 在这些集合中,仅有vector和hashtable是线程安全的,其内部方法基本都有synchronized修饰。 ArrayList 底层采用Object数组实现,实现了RandomAccess接口因此支持随机访问。插入删除操作效率慢。 ArrayList需要一份连续的内存空间。 A…...

手动配置IP
手动配置IP,需要考虑四个配置项: 四个配置项 IP地址、子网掩码、默认网关、DNS服务器 IP地址:格式表现为点分十进制,如192.168.254.1 子网掩码:用于区分网络位和主机位 【子网掩码的二进制表达式一定是连续的&#…...

idea如何使用AI编程提升效率-在IntelliJ IDEA 中安装 GitHub Copilot 插件的步骤-卓伊凡
idea如何使用AI编程提升效率-在IntelliJ IDEA 中安装 GitHub Copilot 插件的步骤-卓伊凡 问题 idea编译器 安装copilot AI工具 实际操作 在 IntelliJ IDEA 中安装 GitHub Copilot 插件的步骤如下: 打开 IntelliJ IDEA: 打开你的 IntelliJ IDEA 应用…...

游戏引擎学习第101天
回顾当前情况 昨天的进度基本上完成了所有内容,但我们还没有进行调试。虽然我们在运行时做的事情大致上是对的,但还是存在一些可能或者确定的bug。正如昨天最后提到的,既然现在时间晚了,就不太适合开始调试,所以今天我…...
css块级元素和行内元素区别
在CSS中,元素可以分为两大类:块级元素(Block-level elements)和行内元素(Inline elements)。这两种元素在网页布局中起着不同的作用,主要体现在它们的显示方式、尺寸控制、以及与其他元素的交互…...

JAVA安全—Shiro反序列化DNS利用链CC利用链AES动态调试
前言 讲了FastJson反序列化的原理和利用链,今天讲一下Shiro的反序列化利用,这个也是目前比较热门的。 原生态反序列化 我们先来复习一下原生态的反序列化,之前也是讲过的,打开我们写过的serialization_demo。代码也很简单&…...
mongodb源码分析session执行handleRequest命令find过程
mongo/transport/service_state_machine.cpp已经分析startSession创建ASIOSession过程,并且验证connection是否超过限制ASIOSession和connection是循环接受客户端命令,把数据流转换成Message,状态转变流程是:State::Created 》 St…...

基于Flask实现的医疗保险欺诈识别监测模型
基于Flask实现的医疗保险欺诈识别监测模型 项目截图 项目简介 社会医疗保险是国家通过立法形式强制实施,由雇主和个人按一定比例缴纳保险费,建立社会医疗保险基金,支付雇员医疗费用的一种医疗保险制度, 它是促进社会文明和进步的…...

YSYX学习记录(八)
C语言,练习0: 先创建一个文件夹,我用的是物理机: 安装build-essential 练习1: 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件,随机修改或删除一部分,之后…...

STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...

如何将联系人从 iPhone 转移到 Android
从 iPhone 换到 Android 手机时,你可能需要保留重要的数据,例如通讯录。好在,将通讯录从 iPhone 转移到 Android 手机非常简单,你可以从本文中学习 6 种可靠的方法,确保随时保持连接,不错过任何信息。 第 1…...

mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包
文章目录 现象:mysql已经安装,但是通过rpm -q 没有找mysql相关的已安装包遇到 rpm 命令找不到已经安装的 MySQL 包时,可能是因为以下几个原因:1.MySQL 不是通过 RPM 包安装的2.RPM 数据库损坏3.使用了不同的包名或路径4.使用其他包…...
laravel8+vue3.0+element-plus搭建方法
创建 laravel8 项目 composer create-project --prefer-dist laravel/laravel laravel8 8.* 安装 laravel/ui composer require laravel/ui 修改 package.json 文件 "devDependencies": {"vue/compiler-sfc": "^3.0.7","axios": …...

三分算法与DeepSeek辅助证明是单峰函数
前置 单峰函数有唯一的最大值,最大值左侧的数值严格单调递增,最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值,最小值左侧的数值严格单调递减,最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...

Scrapy-Redis分布式爬虫架构的可扩展性与容错性增强:基于微服务与容器化的解决方案
在大数据时代,海量数据的采集与处理成为企业和研究机构获取信息的关键环节。Scrapy-Redis作为一种经典的分布式爬虫架构,在处理大规模数据抓取任务时展现出强大的能力。然而,随着业务规模的不断扩大和数据抓取需求的日益复杂,传统…...

Axure 下拉框联动
实现选省、选完省之后选对应省份下的市区...