C++第七节课 运算符重载

一、运算符重载

并不是所有情况下都需要运算符重载,要看这个运算符对这个类是否有意义!

例如:日期减日期可以求得两个日期之间的天数;但是日期 + 日期没有意义!

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//private:
	int _year;
	int _month;
	int _day;
};

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;
}
int main()
{
	Date d1(2023,11,20);
	Date d2(2023,10, 10);
	d1 < d2;   //第一种调用形式
	operator<(d1 , d2); // 两种调用方法都可以(第二种调用形式)
	return 0;
}

在上述例子中我们对<进行重载,使其可以比较两个对象的日期大小!

d1<d2 和operator<(d1,d2)两者是等价的!编译器会将第一种调用形式转化为第二种调用形式;

从底层来看,两者执行的汇编指令是一样的!

但是这里有个问题,在类的外面不能访问private和protect!(上面是将private改为public)

因此,我们可以将函数写在类的内部,这样子方便调用!

但是,直接放入类中会报错:

这是因为:作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this!

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .*(点星,这个运算符很少用) ::(域作用限定符) sizeof ?:(三目) .(成员访问) 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

使用成员函数进行运算符重载如下所示:

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator<(const Date& x)
	{
		if (_year < x._year)
		{
			return true;
		}
		else if (_year == x._year && _month < x._month)
		{
			return true;
		}
		else if (_year == x._year && _month == x._month && _day < x._day)
		{
			return true;
		}
		else
			return false;
	}
	//private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1(2023,11,20);
	Date d2(2023,10, 10);
	d1 < d2;
	// operator<(d1 , d2); // 两种调用方法都可以
	d1.operator<(d2);   // 此时通过成员函数的方式调用!(或者直接比较)
	return 0;
}

返回值类型为bool;

此时调用函数是采用成员函数的方法调用,且依然可以直接进行比较!(这里依然是两个参数,只是第一个this参数被隐藏了!)

接下来我们看两个操作之间的汇编代码,可以发现两者的操作是等价的!

传入的参数必须有一个自定义类型(因为我们不能对内置类型进行重载!)

二、赋值运算符重载

引入:

如果我们想把d2的值赋值给d1,此时我们就需要用到赋值运算符!(这个等号被称为赋值运算符)

  • 拷贝构造是:用一个对象初始化创建另一个对象;
  • 赋值运算符重载是已经存在的两个对象之间的拷贝!

如下所示:

接下来我们尝试写一下最简单版本的赋值重载运算符函数:

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	bool operator<(const Date& x)
	{
		if (_year < x._year)
		{
			return true;
		}
		else if (_year == x._year && _month < x._month)
		{
			return true;
		}
		else if (_year == x._year && _month == x._month && _day < x._day)
		{
			return true;
		}
		else
			return false;
	}
	void operator=(const Date& x)
	{
		_year = x._year;
		_month = x._month;
		_day = x._day;

	}
	//private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1(2023,11,20);
	Date d2(2023,10, 10);
	d1 < d2;
	// operator<(d1 , d2); // 两种调用方法都可以
	d1.operator<(d2);   // 此时通过成员函数的方式调用!(或者直接比较)

	// 将d2的值赋值给d1
	d1 = d2;
	d1.operator=(d2);
	cout << d1._year << endl;
	cout << d1._month << endl;
	cout << d1._day << endl;
	return 0;
}

函数的返回值类型为void!

通过结果发现可以完成对象之间的赋值!(对于日期来说我们需要的就是浅拷贝!)

小插曲:

对C语言来说,支持这样的连续赋值:将0赋值给k(整个返回式的结果为k),k赋值给j(整个返回式的结果为j),j赋值给i(整个返回式的结果为i);

那么两个对象是否可以这样赋值呢?

其实不可以!

自定义类型是转换为对应的两个函数调用,但是这里调用的返回值是void!

正确的操作应该是让右边的表达式返回d4!再将d4的值返回给d5!

因此我们修改下赋值运算符重载函数:

cpp 复制代码
	Date operator=(const Date& x)
	{
		_year = x._year;
		_month = x._month;
		_day = x._day;
		return *this;
	}

例如d4 = d1,这里的返回值就是d4!

其返回值应该是一个数据,且this指针可以在函数内调用,这也是this指针的一大应用!

但是这里的返回类型是对象,因此会调用拷贝构造函数!(每一次重载赋值都要调用一次拷贝构造函数,效率太低下!)

出了作用域,this指针不在,但是*this还存在!因此这里我们建议使用引用返回!(返回的是*this的别名)

因此,最终我们的赋值运算重载函数就从上面进化到了下面:

注意点:所有的指针类型都是内置类型,哪怕是stack* / queue*!

对于下面的代码:

cpp 复制代码
d1 = d1;

此时对于上面的函数体内,this和d实际上都是d1的地址,因此此时赋值没有意义,我们可以将其做以下的修改:(如果相等直接返回任意一个的地址即可,不需要中间赋值,从而提高效率)

cpp 复制代码
	Date operator=(const Date& x)
	{
		if (this != &x)
		// if(*this != x)
		{
			_year = x._year;
			_month = x._month;
			_day = x._day;
		}
			return *this;
	}

如果将if替换为下面的注释行,即比较两个对象是否相等,此时需要调用赋值运算符重载,但是我们实现函数就是为了完成赋值运算符重载!发生报错!

注意:

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

注意:

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

默认生成的赋值重载跟拷贝构造行为一样!

  1. 内置类型成员 -- 值拷贝/浅拷贝;
  2. 自定义类型成员会调用它的赋值重载!

能不能将赋值运算符重载写成一个全局函数?

不能!因为他是默认的成员函数(类中),如果写在外面会与类中的发生冲突! --> 默认成员函数不可以写在全局域中!但是可以类和声明分开写(类中声明,外面定义)!

三、时间类的功能实现

1、获取一个月有多少天的功能实现:

cpp 复制代码
	int GeiMonthDay(int year, int month)
	{
		int daysArr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) && month == 2)
		{
			return 29;
		}
		else
		{
			return daysArr[month];
		}
	}

函数的优化:

第一个优化:

采用下面这种代码形式避免了每次都有先判断是不是闰年,判断闰年的代码较长,先判断闰年效率低下;

第二个优化:

因为该函数在之后需要被频繁调用,直接将开辟好的数组放到静态区有助于避免每次调用都要开辟栈帧,从而提高效率!

搞成内联函数的话也可以,但是编译器不一定会将其变为内联;

引用返回也可以,但是每次返回都是一个整型,只有四个字节,没有必要必须引用返回!且因为返回了整数常量,引用前还需要加上const!太过于麻烦!

一般内置类型传值传参不需要引用!

调整后的函数如下:

cpp 复制代码
int Date::GetMonthDay(int year, int month)
{
	static int daysArr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
	{
		return 29;
	}
	else
	{
		return daysArr[month];
	}
}

2、日期 + 天数

我们给出一个初级的函数代码:

cpp 复制代码
Date Date::operator+(int day)
{
	_day += day;
	while (_day > GetMonthDay(_year,_month))
	{
		_day -= GetMonthDay(_year, _month);
		++_month;
		while (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}
    return *this;
}

但是这个函数存在一些问题!

对一个变量来说,例如:

cpp 复制代码
i+100;

i的值没有发生改变,实际上我们如果传入一个时间,要计算当日之后的100天是几月几号,实际上计算的是+=!(返回的date是+过日期后的date,date的值发生改变)(返回的是一个对象,因此我们这里最好用引用!)

对于内置类型来说,连续的+=可是满足语法条件的!

接下来我们尝试使用+实现:(+不能改变自己)

尽管引用返回会调用拷贝构造函数,降低效率,但是tmp出作用域被销毁,因此必须使用传值返回!(实现+=的时候*this没有被销毁,因此可以返回引用!)

整个表达式的返回值为tmp!

实现了+=之后,再实现+可以通过复用!

同样实现了+之后,再实现+=可以通过复用!

但是两种方法的对象的调用情况不一样:

这里的+=没有创建对象,运用的是引用;然后+创建了一次,返回了一次,共调用两次;

这种情况完成了4次的对象的创建,+中创建tmp再返回tmp(2次);

+=中调用+也创建了两次,一共4次! (调用拷贝构造函数!)(*this不用创建)

3、前置++和后置++的重载

日期类也支持++的操作!

且++是一个单运算符,只有一个操作数!

前置++和后置++的区别:

  • 前置++返回++后的对象;
  • 后置++返回++前的对象;

因此不能用一个函数运算符重载!

class默认的operator++就是前置++,实现代码如下:

cpp 复制代码
Date Date::operator++()
{
	*this += 1;
	return *this;
}

两个重载的运算符能不能构成函数重载?

可以!依然是根据参数不同调用不同的函数,后置++就是通过函数重载实现的!

规定后置++的参数列表中+上一个int类型用来区分!

可以不加上形参,因为这个参数不是用来接受传递值的,仅仅是为了占位,只是用来区分前置++和后置++的,构成函数重载!

当执行++d1的时候,编译器会自动转化为d1.operator() ;

当执行d1++的时候,编译器会自动转化为d1.operator(0) ;

(用来区分)

为什么说前置的效率比后置好?

后置会调用拷贝构造创建tmp,在返回对象tmp时也会调用,而前置++不会调用;

因此前置的效率比后置的高!

总的实现代码如下所示:

Date.h

cpp 复制代码
#pragma once
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1);
	void Print()
	{
		cout << _year << "_" << _month << "_" << _day << endl;

	}
	bool operator<(const Date& x);
	bool operator==(const Date& x);
	bool operator<=(const Date& x);
	bool operator>(const Date& x);
	bool operator>=(const Date& x);
	bool operator!=(const Date& x);

	// 日期 + 时间的返回值还是一个日期
	// 实现获取当前月份的天数
	int GetMonthDay(int year, int month);
	// 实现 日期 + 时间 返回日期
	Date& operator+=(int day);
	Date operator+(int day);
	Date operator++();  // 前置++
	Date operator++(int);  // 后置++

private:
	int _year;
	int _month;
	int _day;
};

Date.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
#include"data.h"

// 分开写拷贝构造函数
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
bool Date::operator<(const Date& x)
{
	if (_year < x._year)
	{
		return true;
	}
	else if (_year == x._year && _month < x._month)
	{
		return true;
	}
	else if (_year == x._year && _month == x._month && _day < x._day)
	{
		return true;
	}
	return false;
}

bool Date::operator==(const Date& x)
{
	return _year == x._year
		&& _month == x._month
		&& _day == x._day;
}

//   d1<d2
//	 this = d1
//	 x = d2
bool Date::operator<=(const Date& x)
{
	return *this < x || *this == x;
}

// 大于就是小于等于取反!
bool Date::operator>(const Date& x)
{
	return !(*this <= x);
}

bool Date::operator>=(const Date& x)
{
	return !(*this < x);
}

bool Date::operator!=(const Date& x)
{
	return !(*this == x);
}
int Date::GetMonthDay(int year, int month)
{
	static int daysArr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	if (month == 2 && (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
	{
		return 29;
	}
	else
	{
		return daysArr[month];
	}
}
Date& Date::operator+=(int day)
{
	// 方法一
	_day += day;
	while (_day > GetMonthDay(_year,_month))
	{
		_day -= GetMonthDay(_year, _month);
		++_month;
		while (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}
	return *this;
	// 方法二
	// *this = *this + day;
	// return *this
}

Date Date::operator+(int day)
{
	// 通过拷贝构造创建一个tmp
	Date tmp(*this);

	// 方法二:复用+=
	tmp += day;   

	// 方法一
	//tmp._day += day;
	//while (tmp._day > GetMonthDay(tmp._year, tmp._month))
	//{
	//	tmp._day -= GetMonthDay(tmp._year, tmp._month);
	//	++tmp._month;
	//	while (tmp._month == 13)
	//	{
	//		++tmp._year;
	//		tmp._month = 1;
	//	}
	//}
	return tmp;
}
Date Date::operator++()   // 前置++
{
	*this += 1;
	return *this;
}

Date Date::operator++(int)  // 后置++
{
	Date tmp = Date(*this);
	*this + -1;
	return tmp;
}

main.cpp

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS
// 运算符重载有一个自定义类型即可
// 不用全部类型都是自定义类型,例如:日期(自定义类西) + 天数(int)
#include"data.h"
void test1()
{
	Date d1(2023, 11, 20);
	d1 + 100;
	d1.Print();


	//Date d2(d1+100);
	Date d2 = d1 + 100;  // 上面两种调用方法都可以!
	d2.Print();


	d1 += 200;
	d1.Print();

}
void test2()
{
	Date d1(2023, 11, 20);
	++d1;
	d1++;
}
int main()
{
	test1();
	return 0;
}
相关推荐
無限進步D3 小时前
Java 运行原理
java·开发语言·入门
是苏浙3 小时前
JDK17新增特性
java·开发语言
SPC的存折5 小时前
1、Redis数据库基础
linux·运维·服务器·数据库·redis·缓存
爱学习的小囧6 小时前
VMware ESXi 6.7U3v 新版特性、驱动集成教程和资源包、部署教程及高频问答详情
运维·服务器·虚拟化·esxi6.7·esxi蟹卡驱动
小疙瘩6 小时前
只是记录自己发布若依分离系统到linux过程中遇到的问题
linux·运维·服务器
dldw7777 小时前
IE无法正常登录windows2000server的FTP服务器
运维·服务器·网络
阿里加多7 小时前
第 4 章:Go 线程模型——GMP 深度解析
java·开发语言·后端·golang
likerhood7 小时前
java中`==`和`.equals()`区别
java·开发语言·python
IronMurphy7 小时前
【算法三十九】994. 腐烂的橘子
算法
我是伪码农7 小时前
外卖餐具智能推荐
linux·服务器·前端