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;
}
相关推荐
waicsdn_haha3 分钟前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
_WndProc5 分钟前
C++ 日志输出
开发语言·c++·算法
薄荷故人_6 分钟前
从零开始的C++之旅——红黑树及其实现
数据结构·c++
m0_748240027 分钟前
Chromium 中chrome.webRequest扩展接口定义c++
网络·c++·chrome
biter00888 分钟前
opencv(15) OpenCV背景减除器(Background Subtractors)学习
人工智能·opencv·学习
qq_4335545414 分钟前
C++ 面向对象编程:+号运算符重载,左移运算符重载
开发语言·c++
良许Linux18 分钟前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网
努力学习编程的伍大侠18 分钟前
基础排序算法
数据结构·c++·算法
蜜獾云28 分钟前
docker 安装雷池WAF防火墙 守护Web服务器
linux·运维·服务器·网络·网络安全·docker·容器
小屁不止是运维29 分钟前
麒麟操作系统服务架构保姆级教程(五)NGINX中间件详解
linux·运维·服务器·nginx·中间件·架构