C++类和对象进阶:运算符重载深度详解

C++类和对象进阶:运算符重载

前言

在C++中,运算符重载允许我们为自定义类型赋予与内置类型相似的操作方式,极大提升了代码的可读性和灵活性。本文将深入探讨运算符重载的规则与实现 ,并重点分析默认成员函数之一的赋值运算符重载函数。

引入

cpp 复制代码
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后面接需要重载的运算符符号

例如要对==进行重载

cpp 复制代码
//假定返回值为bool
bool operator==(int x, int y);	//声明

函数原型:返回值类型 operator操作符(参数列表)

注意事项

  • 不能通过连接其他符号来创建新的操作符:比如operator@。
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义。
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this指针。
  • .*::, sizeof, ? : , . 注意以上5个运算符不能重载。

总结以上内容:

  • 函数格式返回类型 operator运算符(参数列表)
  • 关键限制
    1. 能重载C++中已有的运算符,不能创造新的运算符
    2. 重载运算符主要是针对自定义类型的,因此operator必须有一个类类型参数.
    3. 不能改变运算符对内置类型操作的原始含义
    4. 要重载的运算符有几个操作数,operator中就有几个参数(算上this指针)
    5. 以下运算符不可重载:.* :: sizeof ?: .

重载为全局函数

cpp 复制代码
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;
	else
		return 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

这里会发现运算符重载成全局函数,需要成员变量是公有的,那么问题来了,封装性如何保证?

为解决这一问题,我们可以重载成成员函数。

重载为成员函数

cpp 复制代码
//运算符重载
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修改原变量,加上const
		if (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;
		else
			return 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 :要复合连续赋值的含义
cpp 复制代码
//运算符重载
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) {	//自定义类型,最好传引用,类内不能修改,加上const
		if (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;
		else
			return 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指针了无法访问类内的私有变量,需要给两个参数,如下示例:
cpp 复制代码
Date& operator=(Date& left, const Date& right){
	if (&left != &right){
		left._year = right._year;
		left._month = right._month;
		left._day = right._day;
 	}
 	return left;
 }
  • 赋值运算符如果不显式实现 ,编译器会生成一个默认的。此时用户再在类外自己实现
    一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了 ,故赋值
    运算符重载只能是类的成员函数。

编译器自己生成的赋值运算符重载函数

用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝

注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。

  • 观察以下程序
cpp 复制代码
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函数结束后,会调用st1st2的析构函数,由于地址相同,则会对同一块空间析构两次(对同一块空间free两次)。因此会报错。

需要自己实现的场景

总结默认赋值运算符重载

  • 默认成员函数:默认赋值运算符重载是默认成员函数
  • 重载特性:只能重载成类的成员函数不能重载成全局函数,若定义为全局函数会与编译器默认生成的冲突。
  • 拷贝方式:编译器生成的默认赋值运算符重载函数完成字节序的值拷贝了(浅拷贝)
  • 需要手动实现的场景:如果类中未涉及到资源管理(在堆区申请空间),赋值运算符是否实现都可以;一旦涉及到资源管理则必须要手动实现深拷贝。

拷贝构造函数和赋值重载的区分

思考一下情景,会调用拷贝构造还是赋值重载?

cpp 复制代码
Date d1(2025, 2, 12);
Date d2(2024, 3, 13);
//思考会分别调用什么函数
Date d3 = d1;	//拷贝构造  还是  赋值运算符重载 ?
d2 = d1;		//拷贝构造  还是  赋值运算符重载 ?

验证

这是我们的测试代码

cpp 复制代码
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++拷贝构造与赋值重载的区分

总结:

cpp 复制代码
//区分拷贝构造和赋值运算符重载
Date d1(2025, 2, 12);
Date d2(2024, 3, 13);
//用一个已经存在的对象初始化另一个对象  -----  调用拷贝构造函数
Date d3 = d1;	//等价于 Date d3(d1);

//已经存在的两个对象之间赋值拷贝  -----  赋值运算符重载函数
d2 = d1;
  • 用一个已经存在的对象初始化另一个对象 ----- 调用拷贝构造函数
  • 已经存在的两个对象之间赋值拷贝 ----- 赋值运算符重载函数

总结

特性 运算符重载 赋值运算符重载
必要性 增强代码可读性 资源管理的必要手段
默认行为 浅拷贝
典型应用场景 算术运算、比较运算 对象复制、资源管理
实现要点 操作数类型、返回值优化 深拷贝、自赋值检查

最佳实践建议

  1. 优先使用成员函数形式进行运算符重载
  2. 对于资源管理类必须实现深拷贝赋值
  3. 流操作符(<<, >>)建议采用全局函数+友元形式
  4. 保持运算符的语义一致性(例如+不应修改操作数)

通过合理使用运算符重载,可以显著提升代码的表达能力,使自定义类型的使用更加直观自然。但需谨记"能力越大责任越大",不当的运算符重载反而会降低代码可维护性。
文章到此结束啦,欢迎各位大佬在评论区讨论交流,如果觉得文章写的不错,还请留下免费的赞和收藏

相关推荐
宋康1 分钟前
C/C++ 指针避坑20条
c语言·开发语言·c++
爱丫爱19 分钟前
Python中常见库 PyTorch和Pydantic 讲解
开发语言·pytorch·python
Ryan_Gosling20 分钟前
C++-构造函数-接口
开发语言·c++
ceffans30 分钟前
PDF文档中文本解析
c++·windows·pdf
SummerGao.36 分钟前
Windows 快速搭建C++开发环境,安装C++、CMake、QT、Visual Studio、Setup Factory
c++·windows·qt·cmake·visual studio·setup factory
仟濹40 分钟前
【二分搜索 C/C++】洛谷 P1873 EKO / 砍树
c语言·c++·算法
服务端相声演员1 小时前
Oracle JDK、Open JDK zulu下载地址
java·开发语言
YH_DevJourney1 小时前
Linux-C/C++《C/8、系统信息与系统资源》
linux·c语言·c++
19岁开始学习1 小时前
Go学习-入门
开发语言·学习·golang
青铜念诗2 小时前
python脚本文件设置进程优先级(在.py文件中实现)
开发语言·python