👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
前景回顾
- 构造函数 +析构函数 + 拷贝构造函数
目录
- 前景回顾
- 一、运算符重载
- 1.1 问题引入
- 1.2 运算符重载的概念
- 1.3 运算符重载的注意事项
- 二、赋值运算符重载
- 2.1 赋值运算符重载格式
- 2.2 例子引入
- 2.3 赋值运算符重载的注意事项
- 三、总结
一、运算符重载
1.1 问题引入
当我们实现一个日期类的时候,现在我要比较日期的大小,正常情况下,我们会封装一个比较函数来实现它:
class Date
{
public:
// 构造函数(负责初始化)
Date(int year = 1, int month = 1, int day = 1)
{
this->Year = year;
this->Month = month;
this->Day = day;
}
//private:
int Year; // 年
int Month; // 月
int Day; // 日
};
bool cmp(const Date& x1, const Date& x2)
{
// 假设默认x2的日最大
// 1. 年份大的则大
if (x1.Year < x2.Year)
return true;
// 2. 月份大的则大(前提是年份相等)
else if (x1.Year == x2.Year && x1.Month < x2.Month)
return true;
// 3. 日大则大(前提是年和月份都相等)
else if (x1.Year == x2.Year && x1.Month == x2.Month && x1.Day < x2.Day)
return true;
// 4. 以上要求都没满足返回false
return false;
}
int main()
{
Date d1(2023, 5, 16);
Date d2(2022, 5, 16);
cout << cmp(d1, d2) << endl;
return 0;
}
【程序结果】
以上代码虽然实现了比较两个日期的大小,但有没有发现代码好像有点缺陷 — 可读性差。原因是:
cmp(d1, d2)
这个代码,最后一定会输出1
或0
,问题来了,单单通过1
和0
如何区分d1
大还是d2
大?难道调用者还要到函数定义里查看代码逻辑?因此,为了解决此问题,C++
引入了运算符重载。
1.2 运算符重载的概念
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
- 函数名字为:关键字
operator后面接需要重载的运算符符号
。- 函数原型:返回值类型
operator
操作符(参数列表)
所以,在【问题引入】的代码可以运用运算符重载:
#include <iostream>
using namespace std;
class Date
{
public:
// 构造函数(负责初始化)
Date(int year = 1, int month = 1, int day = 1)
{
this->Year = year;
this->Month = month;
this->Day = day;
}
//private:
int Year; // 年
int Month; // 月
int Day; // 日
};
// 运算符重载
bool operator<(const Date& x1, const Date& x2)
{
// 1. 年份大的则大
if (x1.Year < x2.Year)
return true;
// 2. 月份大的则大(前提是年份相等)
else if (x1.Year == x2.Year && x1.Month < x2.Month)
return true;
// 3. 日大则大(前提是年和月份都相等)
else if (x1.Year == x2.Year && x1.Month == x2.Month && x1.Day < x2.Day)
return true;
// 4. 以上要求都没满足返回false
return false;
}
int main()
{
Date d1(2023, 5, 16);
Date d2(2022, 5, 16);
cout <<(d1 < d2) << endl;
return 0;
}
【程序结果】
几个问题:
- 为什么在65行为什么要对
d1 < d2
加括号?
原因是:流插入操作符(<<·
)的优先级高于<
- 为什么可以直接写
d1 < d2
原因是:d1 < d2
本质上就是在调用operator<
,编译器在编译代码的时候会把d1 < d2
转化为operator<(d1, d2)
,可以通过反汇编来查看代码底层:
1.3 运算符重载的注意事项
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.*:: sizeof ?: .
注意以上5个运算符不能重载。这个经常在笔试选择题中出现
大家有没有发现,在例子1.2中,类中我把成员变量变成公有了,原因是在类外是无法访问私有的成员变量的,那么问题来了,封装性该如何保证?因此以上的代码还得改。既然类外没有访问私有的成员变量的权限,但由于类中的成员函数可以访问私有的成员变量,所以可以将operator<
函数放入类中。
虽然把函数放在类中,但是代码还是编译不过。原因是:当运算符重载作为类成员函数重载时,操作数的个数要等于形参的个数,在注意事项4提到了:作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。因此,完整代码如下:
#include <iostream>
using namespace std;
class Date
{
public:
// 构造函数(负责初始化)
Date(int year = 1, int month = 1, int day = 1)
{
this->Year = year;
this->Month = month;
this->Day = day;
}
// 运算符重载
// di < d2
// 左操作数是this,指向调用函数的对象
bool operator<(const Date& x)
{
// 1. 年份大的则大
if (this->Year < x.Year)
return true;
// 2. 月份大的则大(前提是年份相等)
else if (this->Year == x.Year && this->Month < x.Month)
return true;
// 3. 日大则大(前提是年和月份都相等)
else if (this->Year == x.Year && this->Month == x.Month && this->Day < x.Day)
return true;
// 4. 以上要求都没满足返回false
return false;
}
private:
int Year; // 年
int Month; // 月
int Day; // 日
};
int main()
{
Date d1(2023, 5, 16);
Date d2(2022, 5, 16);
cout << d1.operator<(d2) << endl;
// 上下两行代码本质一样
cout << (d1 < d2) << endl;
return 0;
}
【程序结果】
- 点击查看this指针知识点
二、赋值运算符重载
2.1 赋值运算符重载格式
- 参数类型:
const 类型&
,传递引用可以提高传参效率- 函数名:
operator=
- 返回值类型:
类型&
,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值- 检测是否自己给自己赋值返回*this :要复合连续赋值的含义
2.2 例子引入
#include <iostream>
using namespace std;
class Date
{
public:
// 构造函数(负责初始化)
Date(int year = 1, int month = 1, int day = 1)
{
// 以下的this->均可省略
this->Year = year;
this->Month = month;
Day = day;
}
// 打印函数
void Print()
{
cout << Year << '-' << Month << '-' << Day << endl;
}
// 赋值运算符重载
// d1 = d2;
void operator=(const Date& x)
{
// this是d1的地址
// x是d2的别名
this->Year = x.Year;
this->Month = x.Month;
this->Day = x.Day;
}
private:
int Year;
int Month;
int Day;
};
int main()
{
Date d1(2023, 5, 16);
Date d2(2023, 5, 20);
// 现在要将d2赋值给d1
d1 = d2;// 本质上是d1.operator(d2)
d1.Print();
d2.Print();
return 0;
}
【程序结果】
温馨提示:要注意区分拷贝构造函数和复制拷贝(赋值运算符重载)
- 赋值拷贝(赋值运算符重载):已经存在的两个对象之间的赋值拷贝
- 拷贝构造:是用一个已经存在的对象初始化另一个对象。例如:
Date d3 = d1
但以上的赋值运算符重载还存在一个问题:在C语言中,我们可以进行连续赋值,例如:
周所周知,以上的代码赋值顺序是从右到左的,0
先赋值给k
,然后k = 0
作为返回值,其类型是int
,再赋值给j``,依次类推,但赋值重载是否也能支持连续赋值呢?
#include <iostream>
using namespace std;
class Date
{
public:
// 构造函数(负责初始化)
Date(int year = 1, int month = 1, int day = 1)
{
// 以下的this->均可省略
this->Year = year;
this->Month = month;
Day = day;
}
// 打印函数
void Print()
{
cout << Year << '-' << Month << '-' << Day << endl;
}
// 赋值运算符重载
// d1 = d2;
void operator=(const Date& x)
{
// this是d1的地址
// x是d2的别名
this->Year = x.Year;
this->Month = x.Month;
this->Day = x.Day;
}
private:
int Year;
int Month;
int Day;
};
int main()
{
Date d1(2023, 5, 17);
Date d2, d3;
d3 = d2 = d1;
return 0;
}
【程序报错】
报错原因:注意类中
operator=
的返回值类型是void,因此d1
赋值给d2
后,返回的类型是void
,然后再赋值给d3
,类型不匹配导致报错。
所以,正确的做法应该是返回d1或者是d2的类型,也就是返回Date类型
#include <iostream>
using namespace std;
class Date
{
public:
// 构造函数(负责初始化)
Date(int year = 1, int month = 1, int day = 1)
{
// 以下的this->均可省略
this->Year = year;
this->Month = month;
Day = day;
}
// 打印函数
void Print()
{
cout << Year << '-' << Month << '-' << Day << endl;
}
// 赋值运算符重载
// d1 = d2;
Date& operator=(const Date& x)
{
// this是d1的地址
// x是d2的别名
this->Year = x.Year;
this->Month = x.Month;
this->Day = x.Day;
// 这里返回的是d1的类型
return *this;
}
private:
int Year;
int Month;
int Day;
};
int main()
{
Date d1(2023, 5, 17);
Date d2, d3;
d3 = d2 = d1;
d1.Print();
d2.Print();
d3.Print();
return 0;
}
【程序结果】
- 为什么要用引用做返回值,而不用传值?
在引用章节中提过(点击跳转),传值返回的时候,会生成一个临时变量,而返回值会通过拷贝给临时变量,因此这里会调用一次拷贝构造函数,导致效率低,而用引用做返回值是因为效率高。并且能用引用做返回值的原因是:
*this
就是d1
,在赋值拷贝函数栈帧销毁时,*this
不会跟着销毁,而是在main
函数结束后销毁。
2.3 赋值运算符重载的注意事项
- 赋值运算符只能重载成类的成员函数不能重载成全局函数
因为赋值运算符重载是默认成员函数,如果不在类中自己定义,而在类外定义,编译器会自己生成,就和编译器在类中生成的默认赋值运算符重载冲突。注意:运算符重载不是默认成员函数
- 如果没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
【内置类型成员】
#include <iostream>
using namespace std;
class Date
{
public:
// 构造函数(负责初始化)
Date(int year = 1, int month = 1, int day = 1)
{
// 以下的this->均可省略
this->Year = year;
this->Month = month;
Day = day;
}
// 打印函数
void Print()
{
cout << Year << '-' << Month << '-' << Day << endl;
}
// 如果没有显式实现时,编译器会生成一个默认赋值运算符重载
private:
int Year;
int Month;
int Day;
};
int main()
{
Date d1(2023, 5, 17);
Date d2, d3;
d3 = d2 = d1;
d1.Print();
d2.Print();
d3.Print();
return 0;
}
【程序结果】
【自定义类型成员】
栈是一个经典的要调用对应类的赋值运算符重载完成赋值。理由是:栈的赋值操作只需要将一个栈复制到另一个栈中,不需要进行对象的成员变量的赋值和内存管理的操作,因此使用编译器提供的赋值运算符重载可能会造成不必要的开销和错误。因此,在实现栈的赋值操作时,应该根据栈的特点自行实现赋值运算符重载。
三、总结
-
运算符重载是指在类中定义某些运算符的行为,使得这些运算符可以作用于类的对象。运算符重载可以使得程序更加简洁易读,同时也可以提高代码的可复用性。
-
赋值运算符重载一种特殊的成员函数,也是一种特殊的运算符重载,它用于定义对象之间的赋值操作。默认情况下,C++编译器会自动生成一个默认的赋值运算符,但这个默认的赋值运算符只是进行浅拷贝(内置类型),可能会导致指针成员变量指向同一块内存地址,从而出现意想不到的后果。因此,我们需要手动定义赋值运算符重载,以实现深拷贝(自定义类型),从而保证程序的正确性。