(C++)默认成员函数(中):运算符重载、赋值运算符重载

运算符重载

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
如果不考虑this指针会出现的问题

代码示例

#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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值