目录
一、前言
二、 运算符重载
🍎运算符重载
① 概念引入
② 语法明细
③ 练习巩固
④ 代码展示
🍇赋值运算符重载
① 语法说明及注意事项
② 默认的赋值运算符重载
③ 值运算符不能重载成全局函数!
三、总结
四、共勉
一、前言
【C++】为了增强代码的可读性引入了赋值运算符重载,赋值运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。但是 赋值运算符重载 的应用细节很多和之前讲过的拷贝构造函数数有着千丝万缕的关系,所以本文就来详细的讲解一下赋值运算符重载。
二、 运算符重载
🍎运算符重载
① 概念引入
- 之前呢我们都是对一个日期进行初始化、销毁等操作,现在若是我要去比较一下两个日期,该怎么实现呢?
class Date
{
public:
// 构造函数
Date(int year = 2024,int month = 4,int day = 14)
{
_year = year;
_month = month;
_day = day;
}
//拷贝构造函数
Date(Date& d)
{
cout << "调用拷贝构造" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
std::cout << "year:" << _year << std::endl;
std::cout << "month:" << _year << std::endl;
std::cout << "day:" << _year << std::endl;
}
// 析构函数
~Date()
{
cout << "调用析构构造" << endl;
cout << endl;
_year = 0;
_month = 0;
_day = 0;
}
private:
int _year;
int _month;
int _day;
};
int main()_
{
Date d1(2024, 4, 14);
Date d2(2024, 4, 14);
cout<<(d1==d2)<<endl;
return 0;
}
- 可能你会想到直接这么去写,但是编译器允许吗?很明显它完全实现不了这样的比较
- 于是就想到了把他们封装成为一个函数来进行实现
//等于==
bool Equal(const Date& d1, const Date& d2)
{
//...
}
//小于<
bool Less(const Date& d1, const Date& d2)
{
//...
}
//大于>
bool Greater(const Date& d1, const Date& d2)
{
//...
}
Equal(d1, d2);
Less(d1, d2);
Greater(d1, d2);
- 但是,你认为所有人都会像这样去仔细对函数进行命名吗,尤其是打一些算法竞赛的。它们可能就会把函数命名成下面这样
若是每个函数都是上面这样的命名风格,那么调用的人该多心烦呀╮(╯▽╰)╭
- 如果我们不用考虑函数名,可以直接用最直观的形式也就是一开始讲的那个样子去进行调用的话该多好呀
- 但是呢编译器不认识我们上面所写的这种形式,之前我们去比较两个数的大小或者相等都是
int、char、double
这些【内置类型】的数据,对于这些类型是语法定义的,语言本身就已经存在了的,都将它们写进指令里了 - 不过对于【自定义类型】而言,是我们自己定义的类型,编译器无法去进行识别,也无法去比较像两个日期这样的大小,所以对于自定义类型而言,在于以下两点
- 类型是你定义的,怎么比较,怎么用运算符应该由你来确定
- 自定义类型不一定可以加、减、乘、除,像两个日期相加是毫无意义的,相减的话还能算出他们之间所差天数【日期类中会实现日期加一个天数】
② 语法明细
基于上述的种种问题,C++为了增强代码的可读性引入了运算符重载
【概念】:运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似
【函数名字】:关键字operator
后面接需要重载的运算符符号
【函数原型】:返回值类型 operator 操作符(参数列表)
- 根据上面的语法概念,就可以写出
==
的运算符重载函数
bool operator==(const Date& d1, const Date& d2)
注意事项:
接下去我便通过几点注意实现来带你进一步了解运算符重载
1️⃣:不能通过连接其他符号来创建新的操作符:比如operator@
2️⃣:重载操作符必须有一个类类型(自定义类型)参数
3️⃣:用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 可以看到,我重载了运算符
+
,内部实现了一个乘法运算,然后用当前对象的月数 * 10,最终改变了+
运算符的含义,这种语法虽然是可以编译过的,但是写出来毫无意义,读者可以了解一下
4️⃣:运算符重载可以放在全局,但是不能访问当前类的私有成员变量
- 可以看到,虽然运算符重载我们写出来了,但是在内部调用当前对象的成员 时却报出了错误,说无法访问private的成员,那此时该怎么办呢?
👉解决办法1:去掉[private]
,把成员函数全部设置为公有[public]
👉解决办法2:提供公有函数getYear()
、getMonth()
、getDay()
👉解决办法3:设置友元【不好,会破坏类的完整性】
👉解决办法4:直接把运算符重载放到类内
- 此时我们来试试第四种解决方案,将这个函数放到类内。但是一编译却报出了下面这样的错误,这是为什么呢?【看看下一个点就知道了】
5️⃣:作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- 还记得我们在类和对象的的封装思想中学习过的类成员函数中都存在一个隐藏形参
this
,用于接收调用对象所传入的地址,上面我们在写【日期计算器】的是有也有在类内使用过这个this
指针 - 不过对于
==
来说是个【双目操作符】,其运算符只能有两个,那此时再加上隐藏形参this
的话就会出现问题
bool operator==(Date* this, const Date& d1, const Date& d2)
- 所以当运算符重载函数放到类内时,就要改变其形参个数,否则就会造成参数过多的现象,在形参部分给一个参数即可,比较的对象就是当前调用这个函数的对象即【
this
指针所指对象】与【形参中传入的对象】
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
- 那既然这是一个类内的函数,就可以使用
对象
.的形式去调用,运行结果如下
6️⃣*
、 ::
、 sizeof
、?:
、.
注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
7️⃣运算符重载和函数重载不要混淆了
【运算符重载】:自定义类型对象可以使用运算符
【函数重载】:支持函数名相同,参数不同的函数,同时可以用
③ 练习巩固
上面教了要如何去写
==
的运算符重载,接下去我们就来对其他运算符写一个重载
- 首先就是小于
<
,读者可以试着自己在编译器中写写看
bool operator<(const Date& d)
// 小于
bool operator<(const Date& d)
{
if (_year < d._year) {
return true;
}
else if (_year == d._year && _month < d._month) {
return true;
}
else if (_year == d._year && _month == d._month && _day < d._day) {
return true;
}
else {
return false;
}
}
- 知道了
==
、<
如何去进行重载,那小于等于呢?该如何去实现?
有同学说:这简单,把
<
都改成<=
不就好了
bool operator<(const Date& d)
{
if (_year <= d._year) {
return true;
}
else if (_year == d._year && _month <= d._month) {
return true;
}
else if (_year == d._year && _month == d._month && _day <= d._day) {
return true;
}
else {
return false;
}
}
- 确实上面这样是最直观的形式,但是刚才说了,我们在已经能够写对的情况下要去追求更优的情况。我采取的是下面这种写法,你能很快反应过来吗?
return (*this < d) || (*this == d);
- 其实很简单,我就是做了一个【复用】,使用到了上面重载后的运算符
<
和==
,this
指向当前对象,那么*this
指的就是当前对象,这样来看的话其实就一目了然了
小于、小于等于都会了,那大于
>
和大于等于>=
呢?不等于!=
呢?
bool operator>(const Date& d)
bool operator>=(const Date& d)
bool operator!=(const Date& d)
- 其实上面的这两个都可以用【复用】的思想去进行实现,相信此刻不用我说你应该都知道该如何去实现了把
//大于>
bool operator>(const Date& d)
{
return !(*this <= d);
}
//大于等于>=
bool operator>=(const Date& d)
{
return !(*this < d);
}
//不等于!=
bool operator!=(const Date& d)
{
return !(*this == d);
}
这里就不给出测试结果了,读者可自己修改日期去查看一下
④ 代码展示
class Date
{
public:
// 构造函数
Date(int year = 2024,int month = 4,int day = 14)
{
_year = 2024;
_month = 4;
_day = 14;
}
//拷贝构造函数
Date(Date& d)
{
cout << "调用拷贝构造" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
void Print()
{
std::cout << "year:" << _year << std::endl;
std::cout << "month:" << _year << std::endl;
std::cout << "day:" << _year << std::endl;
}
// 析构函数
~Date()
{
cout << "调用析构构造" << endl;
cout << endl;
_year = 0;
_month = 0;
_day = 0;
}
// 等于== 运算符重载
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
// 小于<
bool operator<(const Date& d)
{
return _year < d._year
|| (_year == d._year && _month < d._month)
|| (_year == d._year && _month == d._month && _day < d._day);
}
// 小于等于 <=
bool operator<=(const Date& d)
{
return (*this < d) || (*this == d);
}
// 大于>
bool operator>(const Date& d)
{
return !(*this <= d);
}
// >=
bool operator>=(const Date d)
{
return (*this < d);
}
private:
int _year;
int _month;
int _day;
};
🍇赋值运算符重载
有了运算符重载的概念后,我们就来讲讲什么是赋值运算符重载
① 语法说明及注意事项
首先给出代码,然后我再一一分解叙述
Date& operator=(const Date& d)
{
if (this != &d) //判断一下是否有给自己赋值
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
👉 【参数类型】:const T&,传递引用可以提高传参效率
- 这一点我在上面讲解拷贝构造的时候已经有重点提到过,加
&
是为了减少传值调用而引发的拷贝构造,加const
则是为了防止当前对象被修改和权限访问的问题,如果忘记了可以再去看看这篇文章:拷贝构造函数
👉 【返回*this】 :要复合连续赋值的含义
- 这块重点讲一下,本来对于赋值运算符来说是不需要有返回值的,设想我们平常在定义一个变量的时候为其进行初始化使用的时候赋值运算,也不会去考虑到什么返回值,但是对于自定义类型来说,我们要去考虑这个返回值
Date d1(2023, 3, 27);
Date d2;
Date d3;
d3 = d2 = d1;
- 可以看到,就是上面这种情况,当d1为d2进行初始化后,还要为d3去进行初始化,那此时就要使用到d2,所以我们在写赋值重载的时候要考虑到返回值的问题,那返回什么呢?
- 因为是为当前对象做赋值,所以应该返回当前对象也就是
*this
👉 【返回值类型】:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 返回值这一块我在引用一文也有相信介绍过,若是返回一个出了当前作用域不会销毁的变量,就可以使用引用返回来减少拷贝构造
-
*this
是当前对象的引用,并不会在函数作用域结束时被销毁。*this
指的是调用成员函数的对象本身,即赋值运算符中operator=
的左操作数。在赋值运算符函数结束后,*this
引用的对象依然存在,因为这个对象的生命周期与原来对象的生命周期一致。
👉 【多方位考虑】:检测是否自己给自己赋值
- 在外界调用这个赋值重载的时候,不免会有人写成下面这种形式
d1 = d1;
- 那自己给自己赋值其实并没有什么意义,所以我们在内部应该去做一个判断,若是当前
this指针
所指向的地址和传入的地址一致的话,就不用做任何事,直接返回当前对象即可。若是不同的话才去执行一个赋值的逻辑
if (this != &d)
知晓了基本写法和注意事项后,我们就来测试运行一下看看是否真的可以完成自定义类型的赋值
- 可以看到 ,确实可以使用
=
去进行日期之间的赋值
- 不仅如此,也可以完成这种【链式】的连续赋值
- 那现在我想问,下面的这两种都属于【赋值重载】吗?
注意:d2 = d1
就是我们刚才说的赋值重载,去类中调用了对应的成员函数;但是对于Date d3 = d2
来说,却没有去调用赋值重载,而是去调用了【拷贝构造】,此时就会有同学很疑惑?
这里一定要区分的一点是,赋值重载是两个已经初始化的对象才可以去做的工作;对于拷贝构造来说是拿一个已经实例化的对象去初始化另一个对象
② 默认的赋值运算符重载
重点地再来谈谈默认的赋值运算符重载,相信在看了构造、析构、拷贝构造后,本小节对你来说不是什么难事😎
- 那还是咱熟悉的老朋友Time类和Date类,在Time类中我写了一个赋值重载
class Time
{
public:
// 构造函数
Time()
{
_hour = 1;
_minute = 1;
_second = 1;
}
// 赋值重载
Time& operator=(const Time& t)
{
if (this != &t)
{
_hour = t._hour;
_minute = t._minute;
_second = t._second;
}
return *this;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int y = 2000, int m = 1, int d = 1)
{
_year = y;
_month = m;
_day = d;
}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
- 我们首先通过如下结果进行观察
- 可以发现,当使用d1初始化d2的时候,去调用了Time类的赋值运算符重载,这是为什么呢?我们其实可以先来看看Date类中的成员变量有:_year、_month、_day以及一个Time类的对象_t,通过上面的学习我们可以知道对于前三者来说都叫做【内置类型】,对于后者来说都叫做【自定义类型】
- 那在构造、析构中我们有说到过对于【内置类型】编译器不会做处理;对于【自定义类型】会去调用默认的构造和析构。在拷贝构造中我们有说到过【内置类型】会按照值拷贝一个字节一个字节;对于【自定义类型】来说会去调用这个成员的拷贝构造
- 那通过上面的调试可以看出赋值运算符重载似乎和拷贝构造是差不多,对于内置类型进行值拷贝,对于自定义类型Time去调用了其赋值重载函数
那我还是一样会提出疑问,既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?
- 没错,也是我们的老朋友Stack类。我想你可能已经猜到了结果😆
typedef int DataType;
class Stack
{
public:
// 构造函数
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_size = 0;
}
// 拷贝构造
Stack(const Stack& st)
{
//根据st的容量大小在堆区开辟出一块相同大小的空间
_array = (DataType*)malloc(sizeof(DataType) * st._capacity);
if (nullptr == _array)
{
perror("fail malloc");
exit(-1);
}
memcpy(_array, st._array, sizeof(DataType) * st._size); //将栈中的内容按字节一一拷贝过去
_size = st._size;
_capacity = st._capacity;
}
// // 赋值重载
//Stack& operator=(const Stack& st)
//{
// if (this != &st)
// {
// memcpy(_array, st._array, sizeof(DataType) * st._size); //将栈中的内容按字节一一拷贝过去
// _size = st._size;
// _capacity = st._capacity;
// }
// return *this;
//}
void Push(const DataType& data)
{
// 扩容...
_array[_size] = data;
++_size;
}
DataType Top()
{
return _array[_size - 1];
}
// 析构函数
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _capacity;
size_t _size;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2;
s2 = s1;
return 0;
}
- 不出所料,程序出现了奔溃,如果你认真看了上面内容的话,就能回忆起上面Stack在进行浅拷贝时出现的问题,和这里的报错是一模一样的
- 我们知道,对于浅拷贝来说是就是一个字节一个字节直接拷贝,和拷贝构造不同的是,两个对象是已经实例化出来了的,
_array
都指向了一块独立的空间,但是在赋值之后,s1和s2的_array
还是指向了同一块空间。此时便会造成两个问题
- 因为它们是同一指向,所以在析构的时候就会造成二次析构
- 原本s2中的
_array
所申请出来的空间没有释放会导致内存泄漏
还是一个画个图来分析一下
📚所以还是一样,当果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现,所以遇到涉及到资源管理的类,就需要自行实现 赋值运算符重载
Stack& operator=(const Stack& st)
{
if (this != &st)
{
memcpy(_array, st._array, sizeof(DataType) * st._size); //将栈中的内容按字节一一拷贝过去
_size = st._size;
_capacity = st._capacity;
}
return *this;
}
③ 值运算符不能重载成全局函数!
- 如下,可以看到我将赋值重载运算符放到了类外来定义,编译一下发现报出了错误,这是为什么呢?其实编译器已经给我们写得很明确了,对于
operator=
也就是赋值重载只能写在类内,不可以写在类外,但一定有同学还是会疑惑为什么要这样规定,且听我娓娓道来~
拿一些权威性的东西来看看,以下是《C++ primer》中的原话
三、总结
【总结一下】:
- 本文我们介绍了两样东西,一个是运算符重载,一个是赋值运算符重载
- 对于运算符重载来说,我们认识到了一个新的关键字operator,使用这个关键字再配合一些运算符封装成为一个函数,便可以实现对自定义类型的种种运算,也加深巩固了我们对前面所学知识的掌握
- 对于赋值运算符重载而言,就是对赋值运算符=进行重载,分析了一些它的有关语法使用特性以及注意事项,也通过调试观察到了它原来和默认拷贝构造的调用机制是一样的,毕竟大家同为天选之子。但是也要区分二者的使用场景,不要混淆了
四、共勉
以下就是我对 赋值运算符重载 的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对C++ 的理解,请持续关注我哦!!!