【C++】—— 类与对象(四)

【C++】------ 类与对象(四)

6、赋值运算符重载

6.1、运算符重载

6.1.1、基础知识

+、--- 、% 、> 等运算符,只对内置有效,但对于自定义类型又该怎么办呢?

就拿 D a t e Date Date类 来说,日期与日期如何比较大小呢?简单地只使用一个 >/< 肯定是无法完成任务的。

解决上述问题,就需要用到 运算符重载

运算符重载的基本特点:

  • 当运算符被用于类类型对象时,C++ 允许我们通过运算符重载的形式制定新的含义。C++ 规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错
  • 运算符重载是具有特殊名字的函数,它的名字是由 o p e r a t o r operator operator 和后面要定义的运算符共同构成。和其他函数一样,它也有返回类型参数列表以及函数体
  • 重载运算符函数的参数个数和该运算符作用的运算对象一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象 向传给第一个参数右测运算对象 传给第二个参数
  • 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的 t h i s this this指针,因此运算符重在作为成员函数是,参数比运算对象少一个

我们先写一个运算符重载函数来判断两个日期是否相等

cpp 复制代码
class Date
{
public:
	Date(int year = 2024, 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)
{
	return  d1._year == d2._year && \
			d1._month == d2._month && \
			d1._day == d2._day;
}

但现在还有一个问题:operator== 在类外,成员变量是私有,没有访问权限

有三种方法:

  1. 把成员变量的访问权限改为公有
  2. 提供 G e t Y e a r GetYear GetYear 等函数
  3. 友元函数(之后讲)

这次,三种方法我们都不用(虚晃一枪),直接把它变成成员函数不香吗

cpp 复制代码
class Date
{
public:
	Date(int year = 2024, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

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

当运算符重载函数是成员函数时,它的第一个参数默认是 t h i s this this指针,因此参数比运算对象少一个。

6.1.2、调用方法

那运算符重载函数怎么调用呢?

它有两种调用方法:

cpp 复制代码
int main()
{
	Date d1(2023, 1, 1);
	Date d2(2024, 1, 1);

	//法一:如普通函数一样的调用方法
	d1.operator==(d2);
	
	//法二:写成运算符的调用方法(推荐)
	d1 == d2;
	
	return 0;
}

6.1.3、前置++ 与 后置++ 的重载

大家有没有想过,++运算符怎么重载呢?毕竟 前置++ 和 后置++ 的运算符重载函数名都是 operator++,并且参数都是一样的 (都为 t h i s this this指针),怎么进行区分呢?

C++ 规定,后置++ 重载时,增加一个 i n t int int 参数,跟 前置++ 构成函数重载,方便区分。

i n t int int 参数后面加不加形参名都可以,实际编译器实际上并不接受,只是为了标识

我们简单写一下 D a t e Date Date 的 前置++后置++

cpp 复制代码
//假设operator+=已经实现

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

//前置++
Date& operator++()
{
	*this += 1;
	return *this;
}

可以看见 后置++前置++ 多了两次拷贝构造,因此对自定义类型,尽量使用前置++,以减少拷贝提高效率

6.1.4、注意事项

  1. 运算符重载后,其优先级结合性与对应的内置类型运算符保持一致。

    重载后,运算符的优先级和结合性该怎样还是怎样,比如 ∗ * ∗ 的优先级依然比 ± 高。

  2. 不能通过链接语法中没有的符号来产生新的操作符:比如 operator@

  3. .* :: sizeof ?: .以上5个运算符不能重载(选择题例常考,大家要记住)

  4. 运算符重载可以构成 函数重载。

    比如对于日期类,以下两个运算符重载就构成函数重载

cpp 复制代码
//以下两个运算符重载虽然函数名相同,但参数不同,构成运算符重载
//日期 - 天数
int operator-(const Date& d, int day);
//日期 - 日期
Date operator-(const Date& d1, const Date& d2);

运算符重载和函数重载虽然都有 "重载" 二字,但他们之间并没有关系。

  • 函数重载指的是函数名相同,参数不同
  • 运算符重载指的是重新定义这个运算符的行为。
  • 再就是 两个运算符重载的函数可以构成函数重载

  1. 重载操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int operator+(int x, int y)

当然,不是全部参数都要自定义类型,有一个即可,比如上面的int operator-(const Date& d, int day);

  1. 一个类需要重载那些运算符,是看哪些运算符重载有意义
    比如 D a t e Date Date类 重载 o p e r a t o r operator operator- 就有意义,重载成 o p e r a t o r operator operator+ 就没有意义(日期 + 日期是什么意思呢)

6.1.5、<< 和 >> 运算符重载

6.1.5.1、<< 和 >> 基础

我们想重载 <<>> 来输入输出日期可不可以呢?我们来试一下

首先我们要知道,coutostream 类型的对象;cinistream 类型的对象

为什么对内置类型 << 能 自动识别类型并输出 呢?

是因为函数重载

类中把所有的内置类型都重载好了,他能自动识别出函数重载,调用相应的函数进行输出

6.1.5.2、日期类 operator<< 的实现

那对于自定义类型我们想继续使用 <<,就需要我们自己重载运算符

我们来写日期类的流插入:

cpp 复制代码
class Date
{
public :
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

	 void operator<<(ostream& out)
	{
		out << _year << "年" << _month << "月" << _day << "号" << endl;
	}

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

上述代码的 o u t out out 是 c o u t cout cout 的引用, o u t out out 就是 c o u t cout cout

我们测试一下:

cpp 复制代码
int main() 
{
	Date d(2024, 1, 1);
	d.operator<<(cout);
		
	return 0;
}

运行结果:

可以看到,没有问题。

6.1.5.2.1、类型不匹配问题

运算符重载还有另外一种调用方式,我们也试一试

cpp 复制代码
int main() 
{
	Date d(2024, 1, 1);
	cout << d;

	return 0;
}

报错了!

为什么呢?两种调用方法不是等价的吗?

别急,我们仔细看一下报错信息:"没有找到接受"Date"类型的右操作数的运算符(或没有可接受的转换)"

简单来说就是 参数不匹配

对于运算符重载函数来说,当操作数为二元操作数,左侧的运算对象默认传给第一个参数右侧的传给的第二个参数。而成员函数的第一个参数默认是 t h i s this this指针,上述的传参方式会导致类型不匹配

因此正确的传参是:

cpp 复制代码
int main()
{
	Date d(2024, 1, 1);
	d << cout;

	return 0;
}

d << cout;这样传参是不是感觉怪怪的,想要cout << d;传参,那只能把 t h i s this this指针 放在第二位ostream& out放在第一位

那有没有办法把this指针放在第二个参数位置上呢?没有。因为 t h i s this this指针 是隐式的,我们无法改变 ,C++规定 t h i s this this指针 默认是第一个参数

因此,operator<< 的重载只能放在类外,成 全局函数 。

但全局函数有一个问题:不能访问私有

我们可以在类中加一个友元声明(下期介绍),这样,类外的函数就可以访问私有啦

cpp 复制代码
class Date
{
	//有元声明
	friend void operator<<(ostream& out, const Date& d);	
	
public :
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
	{}

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

void operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "号" << endl;
}

6.1.5.2.2、连续赋值问题

但现在还有一个问题,它不支持连续输出

cpp 复制代码
int main()
{
	Date d1(2024, 1, 1);
	Date d2(2023, 1, 1);
	
	//无法连续输出
	cout << d1 << d2;

	return 0;
}

怎么解决呢?

首先我们要知道<<运算符的结合性是从左往右的,因此先进行运算的是cout << d1

cout << d1应该要有一个返回值以便能支持接下来 d 2 d2 d2 的输出,那这个返回值是什么呢?很显然是 c o u t cout cout

因此,正确的重载应该是这样:

cpp 复制代码
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "号" << endl;
	return out;
}

这里用 引用返回,返回 o u t out out,即 c o u t cout cout

6.1.5.3、日期类 operator>> 的实现

接下来我们实现流提取

cpp 复制代码
istream& operator>>(istream& in, Date& d)
{
	cout << "请依次输入年月日:>";
	in >> d._year >> d._month >> d._day;
	return in;
}

流提取中,Date& d不能加 c o n s t const const,因为提取出的数据要放在对象里面,是要改变对象的。

6.1.6、总结

  • 当运算符被用于类类型对象时,C++ 允许我们通过运算符重载的形式制定新的含义。C++ 规定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错
  • 运算符重载是具有特殊名字的函数,它的名字是由 o p e r a t o r operator operator 和后面要定义的运算符共同构成。和其他函数一样,它也有返回类型参数列表以及函数体
  • 重载运算符函数的参数个数和该运算符作用的运算对象一样多。一元运算符有一个参数,二元运算符有两个参数,二元运算符的左侧运算对象 向传给第一个参数右测运算对象 传给第二个参数
  • 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的 t h i s this this指针,因此运算符重在作为成员函数是,参数比运算对象少一个
  • 运算符重载后,其优先级结合性与对应的内置类型运算符保持一致
  • 不能通过链接语法中没有的符号来创建新的操作符:比如 operator@
  • .* :: sizeof ?: . 以上5个运算符不能重载(选择题例常考,大家要记住)
  • 重载操作符 至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义
  • 一个类需要重载运算符,是看哪些运算符重载后有意义,比如 D a t e Date Date类 重载 o p e r a t o r operator operator- 就有意义,但是重载 o p e r a t o r operator operator+ 就没有意义
  • 重载++ 运算符时,有 前置++后置++,运算符重载函数名都是 o p e r a t o r operator operator++,无法很好的区分。C++ 规定,后置++ 重载时,增加一个 i n t int int 形参,跟 前置++ 构成函数重载,方面区分
  • 重载 <<>> 时,需要重载为全局函数,因为重载为成员函数,== t h i s this this指针 ==默认抢占了第一个形参位置,第一个形参位置是左侧运算对象,调用时就变成了 对象 << cout,不符合使用习惯和可读性。重载为全局函数把 o s t r e a m ostream ostream / i s t r e a m istream istream 放到第一个形参位置就可以了,第二个形参位置当类类型对象。

6.2、赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象

5.2.1、赋值运算符重载的特点

赋值运算符重载的特点:

  • 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算符重载的参数建议写成 c o n s t const const 当前类类型引用,否则会传值传参会有拷贝
  • 有返回值,且建议写成当前类类型的引用,引用返回可以提高效率 ,有返回值目的是为了支持连续赋值场景

我们先尝试写一个赋值运算符重载

cpp 复制代码
	void operator=(const Date& d)
	{
		_year = d._yaer;
		_month = d._month;
		_day = d.day;
	}

注:赋值运算符重载必须为成员函数

  1. 拷贝构造的参数建议写成引用。但不像调用拷贝构造,赋值重载可以写成传值传参,但传值传参要多调用一次拷贝构造
  2. 赋值重载是有返回值的,返回值建议是自身类类型的引用。为什么呢?主要是存在连续复制的情况

cpp 复制代码
int main()
{
	Date d1(2024, 1, 1);
	Date d2;
	Date d3;

	d3 = d2 = d1;
	return 0;
}

赋值这个运算符是支持连续赋值的,重载后的赋值函数也应支持连续赋值

比如:

cpp 复制代码
int i, j, k;
i = j = k = 1;

这个表达式是怎么执行的呢?

"=" 运算符的结合性是从右往左的;1 赋值给 k k k, k k k = 1 这个表达式是有返回值的,返回左操作数 k k k; k k k 赋值给 j j j,返回 j j j; j j j 再赋值给 i i i,返回 i i i。

所以上述赋值重载正确写法是:

cpp 复制代码
	Date& operator=(const Date& d)
	{
		_year = d._yaer;
		_month = d._month;
		_day = d.day;

		return *this;
	}

这里返回值建议用引用,传值返回的话会多调用 一次拷贝构造

6.2.1、赋值运算符重载进阶

赋值运算符重载的进阶特点

  • 没有显式实现时 ,编译器会自动生成一个默认赋值运算符重载,默认运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造
  • 像 D a t e Date Date 这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显式实现赋值运算符重载。像 S t a c k Stack Stack 这样的类,虽然也都是内置类型,但是 /_ a a a 指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像 M y Q u e u e MyQueue MyQueue 这样的内部主要是自定义类型 S t a c k Stack Stack 成员,编译器自动生成的赋值运算符重载会调用 S t a c k Stack Stack 的赋值运算符重载,也不需要我们显式实现 M y Q u e u e MyQueue MyQueue 的赋值运算重载。这里有一个小技巧,如一个类显式实现了析构并释放了资源,那它就需要显示写赋值运算符重载,否则不需要

这两点特点与拷贝构造是类似的,这里就不再赘述。

6.2.2、赋值重载拷贝与拷贝构造

这里赋值重载拷贝与拷贝构造很容易混淆,我们来看看

需要知道:赋值运算符重载与默认构造函数都是默认成员函数

赋值运算符重载用于完成两个已经存在的对象直接的拷贝赋值,而 拷贝构造是用于一个对象拷贝初始化给另一个要创建的对象

7、日期类的完整实现

下面,是日期类的完整实现,以便大家检验一下类与对象的成果

7.1、Date.h

cpp 复制代码
#pragma once

#include<iostream>
using namespace std;
#include<assert.h>

class Date
{
	//友元声明
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
	
public:
	//检查日期是否合法
	bool CheckDate() const;
	//构造函数
	Date(int year = 2024, int month = 1, int day = 1);
	//打印日期
	void Print() const;

	// 频繁调用,Ĭinline函数
	int GetMonthDay(int year, int month) const
	{
		assert(month > 0 && month < 13);

		static int monthDayArray[13] = { -1, 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;
		}

		return monthDayArray[month];
	}

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

	Date operator+(int day) const;
	Date& operator+=(int day);

	Date operator-(int day) const;
	Date& operator-=(int day);

	// d1++;
	// d1.operator++(0);
	Date operator++(int);

	// ++d1;
	// d1.operator++();
	Date& operator++();

	// d1--;
	// d1.operator--(0);
	Date operator--(int);

	// --d1;
	// d1.operator--();
	Date& operator--();

	// d1 - d2
	int operator-(const Date& d) const;

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

ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);

7.2、Date.cpp

cpp 复制代码
#include"Date.h"

//检查日期是否合法
bool Date::CheckDate() const
{
	if (_month < 1 || _month > 12
		|| _day < 1 || _day > GetMonthDay(_year, _month))
	{
		return false;
	}
	else
	{
		return true;
	}
}

//构造函数
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;

	if (!CheckDate())
	{
		cout << "非法日期:";
		Print();
	}
}

// void Date::Print(const Date* const this)
void Date::Print() const
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

// d1 < d2
bool Date::operator<(const Date& d) const
{
	if (_year < d._year)
	{
		return true;
	}
	else if (_year == d._year)
	{
		if (_month < d._month)
		{
			return true;
		}
		else if (_month == d._month)
		{
			return _day < d._day;
		}
	}

	return false;
}

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

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

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

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

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

// d1 += 100
Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= (-day);
	}

	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		++_month;
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}

	return *this;
}

// d1 + 100
Date Date::operator+(int day) const
{
	Date tmp = *this;
	tmp += day;

	return tmp;
}

// d1 - 100
Date Date::operator-(int day) const
{
	Date tmp = *this;
	tmp -= day;

	return tmp;
}

// d1 -= 100
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += (-day);
	}

	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			_month = 12;
			--_year;
		}

		_day += GetMonthDay(_year, _month);
	}

	return *this;
}

// d1++;
// d1.operator++(0);
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;
	return tmp;
}

// ++d1;
// d1.operator++();
Date& Date::operator++()
{
	*this += 1;
	return *this;
}

// d1 - d2
int Date::operator-(const Date& d) const
{
	int flag = 1;
	Date max = *this;
	Date min = d;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}

	int n = 0;
	while (min != max)
	{
		++min;
		++n;
	}

	return n * flag;
}

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
	return out;
}

istream& operator>>(istream& in, Date& d)
{
	while (1)
	{
		cout << "请依次输入年月日:>";
		in >> d._year >> d._month >> d._day;

		if (!d.CheckDate())
		{
			cout << "输入日期非法:";
			d.Print();
			cout << "请重新输入!!!" << endl;
		}
		else
		{
			break;
		}
	}

	return in;
}

注:

  1. int GetMonthDay(int year, int month) const是用来获取该年该月日期,因为需要多次调用,因此直接在类内实现成内联函数
  2. o p e e r a t o r opeerator opeerator+ 可通过 o p e e r a t o r opeerator opeerator+= 复用实现。 o p e e r a t o r opeerator opeerator+= 可通过 o p e e r a t o r opeerator opeerator+ 复用实现,但更推荐前者( o p e r a t o r operator operator+ 达到实现效率一致, o p e e r a t o r opeerator opeerator+=
    后者要多调用两次拷贝构造和一次赋值)
  3. 对于 <,<=,==,>,>=,!= ,他们 6 个 任意实现 2 个即可,其他 4 个均可 通过逻辑间相互复用实现


好啦,本期关于类和对象的知识就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在C语言的学习路上一起进步!

相关推荐
羊小猪~~3 分钟前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio
binishuaio9 分钟前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE11 分钟前
【Java SE】StringBuffer
java·开发语言
就是有点傻15 分钟前
WPF中的依赖属性
开发语言·wpf
洋24023 分钟前
C语言常用标准库函数
c语言·开发语言
进击的六角龙25 分钟前
Python中处理Excel的基本概念(如工作簿、工作表等)
开发语言·python·excel
wrx繁星点点26 分钟前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式
NoneCoder43 分钟前
Java企业级开发系列(1)
java·开发语言·spring·团队开发·开发
苏三有春43 分钟前
PyQt5实战——UTF-8编码器功能的实现(六)
开发语言·qt
脉牛杂德1 小时前
多项式加法——C语言
数据结构·c++·算法