默认成员函数(中):运算符重载和运算符赋值重载
运算符重载
1、理解
内置类型的运算,编译器是有对应的指令完成的。比如int a=5,int b=10,写出a<b,编译器知道怎么去运算。
但自定义类型比如class Date的对象d1和d2,如果写出d1<d2,编译器也不知道该怎么算。
所以,自定义类型的运算需要我们自己写函数定义——这就是运算符重载。
写了运算符重载函数以后,编译器再遇到相应的运算,就会自动调用相应的运算符重载函数。当然也可以选择显示调用运算符重载函数。
2、写法、规则与特性
(1)运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体(结合实际意义去写!)。
(2)重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。⼀元运算符有⼀个参数,⼆元运算符有两个参数,⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第⼆个参数。
(3)如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。
(4)运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
(5)不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@。
(6)不能重载的五个运算符
- ::(域运算符):用于访问命名空间、类或枚举中的成员,不能被重载。
- . 成员访问运算符
.*和->*(成员指针访问运算符):用于通过指针访问对象的成员,不能被重载。(单个->可以重载!!)
用法示例
class MyClass
{
public:
int data;
};
int main()
{
MyClass obj;
int MyClass::*ptr = &MyClass::data;
obj.*ptr = 10; // 使用.*运算符访问成员变量
MyClass* pObj = &obj;
pObj->*ptr = 20; // 使用->*运算符访问成员变量
return 0;
}
- ?:(条件运算符):用于根据条件表达式的值选择两个表达式中的一个,不能被重载。
- sizeof(大小运算符):用于获取变量或类型的大小,不能被重载。
- typeid(类型信息运算符):用于获取变量或类型的类型信息,不能被重载。
(7)重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator(int x,int y)这样是不行的!!!
(8)⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,⽐如Date类重载operator-就有意义,但是重载operator*就没有意义。
(9)重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。
//前置++
Date operator++()
//后置++
Date operator++(int)
3、类型
(1)在全局域定义的运算符重载函数
代码示例(写法思路在代码注释中说明)
#include <iostream>
using namespace std;
class Date
{
public:
//构造函数
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
//打印日期函数
void Print() const {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
//在全局域进行运算符==的重载
//根据实际意义(比较日期)确定返回类型(bool,确定是或否)、返回值
//根据==作用的运算对象数量和顺序确定参数列表(二元运算符,左右为被比较数),能传引用尽量用传引用(减少拷贝数量),const修饰引用类型防止对象传入运算时被修改。
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year
&& d1._month == d2._month
&& d1._day == d2._day;
}
int main() {
Date d1(2024, 1, 3);
Date d2(2025, 12, 25);
if (d1 == d2)
//出现Date类对象使用运算符==,编译器自动调用Date类的==重载函数
//也可以显示调用,即operator==(d1,d2)
{
cout << "日期相同" << endl;
}
else{
cout<< "日期不同" << endl;
return 0;
}
但是这样的代码面临一些问题!

即在全局域定义的函数无法调用类私有的成员变量!对此有以下几种解决方法
a、成员放公有(挺方便的但太简单粗暴了,意味着所有情况都可以随意访问、读取、修改类的成员了)
b、Date提供getxxx const函数
(思维是通过公有的成员函数来访问私有的成员变量,但只提供读的权限,不可修改,比a方法安全,保护了类的封装性)
c、友元函数(暂不作说明,但会一定程度上破坏类的封装性)
我们这里重点写b方法
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
int GetYear() const
{
return _year;
}
int GetMonth() const
{
return _month;
}
int GetDay() const
{
return _day;
}
//打印日期函数
void Print() const {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
bool operator==(const Date& d1, const Date& d2)
{
return d1.GetYear() == d2.GetYear()
&& d1.GetMonth() == d2.GetMonth()
&& d1.GetDay() == d2.GetDay();
}
int main() {
Date d1(2024, 1, 3);
Date d2(2025, 12, 25);
if (d1 == d2) {
cout << "日期相同" << endl;
}
else {
cout << "日期不同" << endl;
}
return 0;
}
(2)重载为成员函数(最常用的操作符重载方式)
如果我们直接把操作符重载成成员函数,上面无法调用类私有的成员变量的问题自然就不存在了!我们需要注意的就是成员函数具有隐含参数this指针,所以我们在写operator函数时,实际写的参数数量=运算符运算对象数量-1

代码示例
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
//打印日期函数
void Print() const {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2024, 1, 3);
Date d2(2025, 12, 25);
if (d1 == d2)
//自动进入运算符重载,显示表现的话就是d1.operator==(d2)
{
cout << "日期相同" << endl;
}
else {
cout << "日期不同" << endl;
}
return 0;
}
4、运算符重载的函数重载
函数重载指同名函数可以有不同的参数列表
运算符重载指让自定义类型可以使用运算符完成有意义的运算
在一定情况下,运算符重载函数可能构成函数重载(相同的运算符由于参数不同、意义不同对应了不同的运算方法)举例如下,这两个函数都是对-的运算符重载,但不同参数类型可以重载至不同函数。

5、加深理解运算符重载:基于运算符重载的顺序表读写
(1)可读:实现像遍历数组一样遍历顺序表
#include <iostream>
using namespace std;
class SeqList
{
public:
SeqList(int n = 4)
{
_a = (int*)malloc(sizeof(int)*n);
//检查资源分配是否成功,此处暂略去不写
_size = 0;
_capacity = 4;
}
~SeqList()
{
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
int operator[](size_t i) //数组访问运算符也是个二元运算符
{
return _a[i];
}
void PushBack(int x)
{
//扩容
//……
_a[_size++] = x;
}
int size() {
return _size;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
SeqList s;
s.PushBack(1);
s.PushBack(2);
s.PushBack(3);
//读:可以像遍历数组一样遍历顺序表
for (int i = 0;i < s.size();i++)
{
cout << s[i] << " ";
}
cout << endl;
return 0;
}
(2)可写:实现像修改数组一样修改顺序表
注意要可修改,所以运算符重载要返回引用类型!!!
#include <iostream>
using namespace std;
class SeqList
{
public:
SeqList(int n = 4)
{
_a = (int*)malloc(sizeof(int)*n);
//检查资源分配是否成功,此处暂略去不写
_size = 0;
_capacity = 4;
}
~SeqList()
{
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
int& operator[](size_t i)
//因为我们想对顺序表进行修改,所以应该返回引用类型!!!
{
return _a[i];
}
void PushBack(int x)
{
//扩容
//……
_a[_size++] = x;
}
int size() {
return _size;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
SeqList s;
s.PushBack(1);
s.PushBack(2);
s.PushBack(3);
//可读:可以像遍历数组一样遍历顺序表
for (int i = 0;i < s.size();i++)
{
cout << s[i] << " ";
}
cout << endl;
//可写:修改顺序表
for (int i = 0;i < s.size();i++)
{
s[i] += 10;
}
cout << endl;
for (int i = 0;i < s.size();i++)
{
cout << s[i] << " ";
}
cout << endl;
return 0;
}
运行结果

赋值运算符重载(默认成员函数)
1、理解
赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象直接的拷贝赋值,这⾥要注意跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个要创建的对象。
2、写法
(1) 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引⽤,否则传值传参会有拷贝。
(2)有返回值,且建议写成当前类类型引用,引⽤返回可以提⾼效率(减少拷贝),有返回值目的是支持连续赋值场景。
代码示例
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1) {
_year = year;
_month = month;
_day = day;
}
//赋值运算符重载
Date& operator=(Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
//d2给d1赋值,返回的是d1对象,也就是this指针解引用
//返回值是为了解决连续赋值问题
}
//打印日期函数
void Print() const {
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1(2024, 1, 3);
Date d2(2025, 12, 25);
Date d3(2025, 10, 10);
d1.Print();
d2.Print();
cout << endl;
d1 = d2;
d1.Print();
d2.Print();
cout << endl;
d1 = d2 = d3;//连续赋值问题
//希望的运算思路是d3赋值给d2,并且d2=d3的结果是d2,再把d2赋值给d1
//所以运算符重载需要返回值,且返回的是=左边的对象
d1.Print();
d2.Print();
d3.Print();
return 0;
}
运行结果

3、什么时候需要自己写赋值运算符重载函数
(结合(上)中的Date、Stack、MyQueue类说明)
像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显⽰实现赋值运算符重载。
像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。
像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的赋值运算符重载会调⽤Stack的赋值运算符重载,也不需要我们显⽰实现MyQueue的赋值运算符重载。
这⾥还有⼀个小技巧,如果⼀个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。
也就是说写了析构并释放资源,那么拷贝构造、赋值运算符重载都要写。
4、补充说明
(1)注意事项
Date d1=d2;这是拷贝构造
d1、d2已定义,直接d1=d2,这是赋值运算符重载!
拷贝构造这么写的意义还是增强可读性
给ret赋值,还是第二种写法可读性更强
(2)对于Date、Stack、MyQueue的默认成员函数是否需要自己书写的总结
| 类 | 构造函数 | 析构函数 | 拷贝构造函数 | 赋值运算符重载函数 |
|---|---|---|---|---|
| Date | 是 | 否 | 否 | 否 |
| Stack | 是 | 是 | 是 | 是 |
| MyQueue | 否 | 否 | 否 | 否 |
默认成员函数(中):运算符重载、赋值运算符重载&spm=1001.2101.3001.5002&articleId=147709752&d=1&t=3&u=a014be9d9dac4a81b4f4abec2a74814d)
3849

被折叠的 条评论
为什么被折叠?



