【C++杂货铺】运算符重载

目录

前言

本文将以日期类为基础,去探寻运算符重载的特性与使用方法,下面先给出日期类的基础定义:

cpp 复制代码
class Date
{
public:
	Date::Date(int year, int month, int day)
	{
		if (month > 0 && month <= 12
			&& day > 0 && day <= GetDay(year, month))
		{
			_year = year;
			_month = month;
			_day = day;
		}
		else
		{
			cout << "非法日期" << endl;
			assert(false);
		}
	}
private:
	int _year;//年
	int _month;//月
	int _day;//日
};

备注:拷贝构造函数和析构函数,均可以不写,因为当前日期类的三个成员变量都是内置类型,没有动态申请空间,使用浅拷贝就可以。

一、运算符重载

📖如何比较两个日期的大小?

cpp 复制代码
int main()
{
	Date d1(2023, 7, 21);
	Date d2(2023, 6, 21);
	return 0;
}

现如今,定义了两个日期类的对象d1d2,该如何比较这两个对现象的大小呢?首先想到的是,写一个函数来比较他俩的大小,向下面这样:

cpp 复制代码
//以小于比较为例
bool Less(const Date& x, const Date& y)
{
	if (x._year > y._year)
	{
		return false;
	}
	else if (x._year == y._year && x._month > y._month)
	{
		return false;
	}
	else if (x._year == y._year && x._month == y._month && x._day > y._day)
	{
		return false;
	}
	else
	{
		return true;
	}	
}

存在的问题 :首先这个函数是写在类外面的,意味着,日期类的成员变量如果是private私有的话,在类外面就无法访问,所以在这个函数里面是访问不到对象的年、月、日这三个成员变量,即x._year等都是非法的,要想实现该函数的功能,日期类的成员变量必须是public公有。

其次,在比较两个日期类对象大小的时候,需要写成Less(d1, d2),这和我们平时直接用<符号比较大小,比起来不够直观。

📖为什么日期类不能直接使用<

因为日期类是我们自己定义的,属于一种自定义类型,它的大小比较方式,只有定义它的人知道,而像intdouble等内置类型,是祖师爷创造C++语言时就定好的,祖师爷当然知道该如何比较两个内置类型变量的大小,所以提前帮我们设置好了,我们可以直接用<去比较两个内置类型变量的大小,而至于祖师爷是怎么设置的,这里先埋一个伏笔。

📖运算符重载

为了解决上面Less函数存在的问题,C++引入了运算符重载,它可以让我们直接使用<来比较两个日期类的大小。

运算符重载是具有特殊函数名的函数,也具有返回值类型,函数名字、参数列表、返回值类型都和普通函数类似。

  • 函数名字 :关键字operator后面接需要重载的运算符符号。
  • 函数原型:返回值类型 operator操作符(参数列表)
cpp 复制代码
bool operator<(const Date& x, const Date& y)
{
	if (x._year > y._year)
	{
		return false;
	}
	else if (x._year == y._year && x._month > y._month)
	{
		return false;
	}
	else if (x._year == y._year && x._month == y._month && x._day > y._day)
	{
		return false;
	}
	else
	{
		return true;
	}
}

上面就是对<运算符的一个重载,它的两个形参是Data类型的引用,此时两个日期类对象就可以直接用<来比较大小啦,d1 < d2本质上就是调用运算符重载函数,但是由于上面的运算符重载函数还是写在类外面,所以当日期类的成员变量是private私有的时候,该运算符重载函数还是用不了。

cpp 复制代码
//下面两条语句是等价的本质都是调用运算符重载函数
d1 < d2;
operator<(d1, d2);//d1 < d2的本质

📖将运算符重载函数写成成员函数

为了解决上面的私有成员变量在类外面无法访问的问题,可以把运算符重载函数写成类的成员函数 或者友元,这样就能访问到私有的成员变量,但是友元一般不建议使用,因为友元会破坏封装。

cpp 复制代码
bool operator<(const Date& d) 
{
	if (_year < d._year)
	{
		return true;
	}
	else if (_year == d._year && _month < d._month)
	{
		return true;
	}
	else if (_year == d._year && _month < d._month && _day < d._day)
	{
		return true;
	}
	else
	{
		return false;
	}
}

上面就是把<运算符重载成类的成员函数,此时参数只有一个,因为<是一个双目运算符,类的非静态成员函数有一个隐藏的形参this指针,所以形参就只需要一个。

cpp 复制代码
//它们俩是等价的
d1 < d2;
d1.operator<(d2);//d1 < d2的本质


小Tips :一个双目运算符如果重载成类的成员函数,会把它的左操作数传给第一个形参,把右操作数传给第二个形参。以上面为例,this指针接收的是d1的地址,d接收的是d2

📖注意事项:

  • 不能通过连接其他符号来创建新的运算符:比如operator@
  • 重载操作符必须有一个类类型参数。
  • 用于内置类型的运算符,其含义不能改变,例如:内置的+,不能改变其含义。
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .*::sizeof? :.这五个运算符不能重载。

二、赋值运算符重载

📖区分赋值运算符重载和拷贝构造

cpp 复制代码
Date d1(2020, 5, 21);
Date d2(2023, 6, 21);
d1 = d2;//需要调用赋值运算符重载
Date d3 = d1;//这里是调用拷贝构造函数
//Date d3(d1);//和上一行等价调用拷贝构造

要区分赋值运算符重载拷贝构造,前者是针对两个已存在的对象,将一个对象的值,赋值给另一个,而后者是用一个已存在的对象去初始化创建一个新对象。

赋值运算符重载格式:

  • 参数类型const T&(T是类型),传引用返回可以提高效率。
  • 返回值类型T&,返回引用可以提高效率,有返回值目的是为了支持连续赋值。
  • 检测是否自己给自己赋值
  • 返回*this:要符合连续赋值的含义。
cpp 复制代码
Date& operator=(const Data& d)
{
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	return *this;//出了作用域*this还在,所以可以用引用返回
}

📖只能是类的成员函数

上面的<运算符,最开始我们是在类外面把它重载成全局的,后来为了保证类的封装性,才把它重载成类的成员函数,而赋值运算符天生只能重载成类的成员函数 ,因为赋值运算符重载属于类的默认成员函数,我们不写,编译器会自动生成,所以,如果我们把赋值运算符重载写在类外面,就会和编译器生成的默认赋值运算符重载发生冲突。

📖编译器生成的干了些什么工作?

用户没有显式实现时,编译器生成的默认赋值运算符重载,对内置类型的成员变量是以值的方式逐字节进行拷贝(浅拷贝),对自定义类型的成员变量,调用其对应类的赋值运算符重载。

三、完善日期类

有了上面的基础,接下来完善一下日期类,重载其他的运算符。

3.1 重载关系运算符

关系运算符有<>==<=>=!=,由于它们之间存在的逻辑关系,可以通过复用来实现,即:要想知道a是否大于b,可以通过判断a是否小于等于b来实现。因此,我们只要写一个<==的比较逻辑,其他的直接复用即可。

📖重载<

cpp 复制代码
bool operator<(const Date& d)
{
	if (_year < d._year)
	{
		return true;
	}
	else if (_year == d._year && _month < d._month)
	{
		return true;
	}
	else if (_year == d._year && _month < d._month && _day < d._day)
	{
		return true;
	}
	else
	{
		return false;
	}
}

📖重载==

cpp 复制代码
bool Date::operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

📖重载<=

cpp 复制代码
bool Date::operator<=(const Date& d)
{
	return *this < d || *this == d;
}

📖重载>

cpp 复制代码
bool Date::operator>(const Date& d)
{
	return !(*this <= d);
}

📖重载>=

cpp 复制代码
bool Date::operator>=(const Date& d) 
{
	return !(*this < d);
}

📖重载!=

cpp 复制代码
bool Date::operator!=(const Date& d)
{
	return !(*this == d);
}

3.2 重载++=

有时我们需要知道几天之后的日期,比如我想知道100天后的日期,此时就需要用当前的日期加上100,但是一个日期类型和一个整型可以相加嘛?答案是肯定的,可以通过重载+来实现。运算符重载只规定必须有一个类类型参数,并没有说重载双目操作符必须要两个类型一样的参数。

📖获取某月的天数

日期加天数,要实现日期的进位,即:当当前日期是这个月的最后一天时,再加一天月份就要进一,当当前的日期是12月31日时,再加一天年份就要进一,因此可以先实现一个函数,用来获取当前月份的天数,在每加一天后,判断月份是否需要进位。

cpp 复制代码
int GetDay(int year, int month)//获取某一月的天数
{
	static int arr[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;
	}

	return arr[month];
}

除了2月,每个月的天数都是固定的,因此可以设置一个数组来存放每个月的天数,并且以月份作为下标,对应存储该月的天数,这种方法类似于哈希映射。这里还有两个小细节,第一个:把数组设置成静态,因为这个函数会重复调用多次,把数组设置成静态,它第一次创建之后,一直到程序结束都还在,可以避免函数调用时重复的创建数组。第二点:把month == 2放在前面判断,因为只有当2月的时候才需要判断是否是闰年,如果不是2月就不用判断是不是闰年。

📖重载+

cpp 复制代码
Date Date::operator+(int x)
{
	if(x < 0)//天数为负的时候
	{
		return *this - (-x);//复用-
	}
	/Date tmp = *this;
	//Date tmp(*this);//和上面等价,都是调用拷贝构造函数
	
	tmp._day = _day + x;
	while (tmp._day > GetDay(tmp._year, tmp._month))
	{
		tmp._day = tmp._day - GetDay(tmp._year, tmp._month);
		tmp._month++;
		if (tmp._month == 13)
		{
			tmp._year++;
			tmp._month = 1;
		}
	}
	return tmp;//
}

注意 :要计算a+b的结果,a是不能改变的,因此一个日期加天数,不能改变原本的日期,也就是不能修改this指针指向的内容,所以我们要先利用拷贝构造函数创建一个和*this一模一样的对象,对应上面代码中的tmp,在该对象的基础上去加天数。出了作用域tmp对象会销毁,所以不能传引用返回。

📖重载+=
+=+很像,区别在于+=是在原来是日期上进行修改,即直接对this指针指向的日期做修改,所以我们对上面的代码稍作修改就可以得到+=

cpp 复制代码
Date& Date::operator+=(int x)
{
	if (x < 0)//当天数为负
	{
		return *this -= -x;//复用-=
	}
	_day += x;
	
	while (_day > GetDay(_year, _month))
	{
		_day = _day - GetDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}

小Tips :加一个负的天数,就是算多少天以前的日期,所以,当天数为负的时候,可以复用下面的-=

📖**++=之间的复用**

可以发现,++=的实现方法十分相似,那是否可以考虑复用呢?答案是肯定的,他俩其中的一方都可以去复用另一方。

+去复用+=

cpp 复制代码
Date Date::operator+(int x)
{
	/Date tmp = *this;
	//Date tmp(*this);//和上面等价,都是调用拷贝构造函数
	tmp += x;
	return tmp;//
}

+=去复用+

cpp 复制代码
Date& Date::operator+=(int x)
{
	*this = *this + x;//这里是调用赋值运算符重载
	return *this;
}

注意:上面的两种复用,只能存在一个,不能同时都去复用,同时存在会出现你调用我,我调用你的死穴。

既然只能存在一个,那到底该让谁去复用呢?答案是:+去复用+= 。因为,+=原本的实现过程中并没有调用拷贝构造去创建新的对象,而+原本的实现过程中,会去调用拷贝构造函数创建新的对象,并且是以值传递的方式返回的,期间又会调用拷贝构造。如果让+=去复用+,原本还无需调用拷贝构造,复用后反而还要调用拷贝构造创建新对象,造成了没必要的浪费。

3.3 重载--=

有时我们也需要知道,多少天以前的日期,此时就需要重载-,它的两个操作数分别是日期和天数,其次,我们有时还想知道两个日期之间隔了多少天,这也需要重载-,但此时的两个操作数都是日期。两个-重载构成了函数重载。

📖重载日期-天数

有了上面的经验,我们可以先重载-=,再让-去复用-=即可,日期减天数,就是要实现日期的借位。

cpp 复制代码
Date Date::operator-(int x) 
{
	Date tmp(*this);

	return tmp -= x;//复用-=
}

📖重载-=

cpp 复制代码
Date& operator-=(int x)
{
	if (x < 0)//天数天数小于0
	{
		return *this += -x;//复用+=
	}
	_day -= x;
	while (_day <= 0)
	{
		_month--;
		if (_month == 0)
		{
			_month = 12;
			_year--;
		}
		_day += GetDay(_year, _month);
	}

	return *this;
}

📖重载日期-日期

日期-日期,它的形参是一个日期对象,计算的结果是两个日期之间的天数,所以返回值是int,要像知道两个日期之间相隔的天数,可以设置一个计数器,让小日期一直加到大日期,就可以知道两个日期之间相隔的天数。

cpp 复制代码
int operator-(const Date& d) 
{
	Date max = *this;//存放大日期
	Date min = d;//存放小日期
	int flag = 1;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int n = 0;
	while (max != min)
	{
		--max;
		++n;
	}

	return n * flag;
}

3.4 重载++--

++--操作符,无论前置还是后置,都是一元运算符,为了让前置和后置形成正确的重载,C++规定:后置重载的时候多增加一个int类型的参数,但是当使用后置,调用运算符重载函数时该参数不用传递,编译器自动传递。

📖重载前置++

cpp 复制代码
//前置++,返回++之后的值
Date& Date::operator++()
{
	return *this += 1;//直接复用+=
}

📖重载后置++

cpp 复制代码
//后置++,返回加之前的值
Date Date::operator++(int)//编译器会把有int的视为后置++
{
	Date tmp(*this);
	*this += 1;//复用+=
	return tmp;
}

📖重载前置--

cpp 复制代码
Date& operator--()
{
	return *this -= 1;//复用了-=
}

📖重载后置--

cpp 复制代码
Date operator--(int)
{
	Date tmp(*this);
	*this -= 1;//复用了-=
	return tmp;
}

对比前置和后置可以发现,后置会调用两次拷贝构造函数,一次在创建tmp的时候,另一次在函数返回的时候。而前置则没有调用拷贝构造,所以前置的效率相比后置会高那么一点。

3.5 重载<<>>

同理,对于自定义类型,编译器仍然不知道如何打印,所以要想通过<<去直接打印日期类对象,需要我们对<<运算符进行重载。

📖重识coutcin

我们在使用C++进行输入输出的时候,会用到cincout,它们俩本质上都是对象,cinistream类实例化的对象,coutostream类实例化的对象。

内置类型可以直接使用<<>>,本质上是因为库中进行运算符重载。而<<>>不用像C语言的printfscanf那样,int对应%dfloat对应%f,是因为运算符重载本质上是函数,对这些不同的内置类型,分别进行了封装,在运算符重载的基础上又实现了函数重载,所以<<>>支持自动识别类型。

📖**<<为什么不能重载成成员函数**

要实现对日期类的<<,要对<<进行重载。但是<<和其他的运算符有所不同,上面重载的所有运算符,为了保证类的封装性,都重载成了类的成员函数,但是<<不行,因为我们平时的使用习惯是cout << d1,前面说过,对于一个双目运算符的重载,它的左操作数会传递给运算符重载函数的第一个形参,右操作数会传递给运算符重载函数的第二个形参,也就是说cout会传递给第一个形参,日期类对象d2会传递给第二个形参,如果运算符重载函数是类的成员函数的话,那么它的第一个形参是默认的this指针,该指针是日期类类型的指针,和cout的类型不匹配,当然也有解决办法,那就是输出一个日期类对象的时候,写成d1 << cout,此时就相当于d1.operator(cout),会把d1的地址传给this指针,形参再用一个ostream类型的对象来接收cout即可,但是这样的使用方式,显然是不合常理的。

📖<<重载成全局函数

正确的做法是,把<<重载成全局函数,此时函数形参就没有默认的this指针,我们可以根据需要来设置形参的顺序,第一个形参用ostream类对象来接收cout,第二个形参用Date日期类对象来接收d1

cpp 复制代码
//重载成全局的
ostream& operator<< (ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;

	return out;
}

注意 :形参out不能加const修饰,因为我们就是要往out里面写东西,加了const意味着out不能修改。其次为了实现连续的输出,返回值是ostream类型的对象out,因为此时出了作用域out还在,所以可以用引用返回。

因为该运算符重载函数写在全局,默认情况下,在该函数内部是无法访问到日期类的私有成员变量,为了解决这个问题,可以把该运算符重载函数设置成友元函数 ,或者在类里面写私有成员变量的Get方法(Java常用)。

cpp 复制代码
friend ostream& operator<< (ostream& out, Date& d);

友元函数只需要配合上friend关键字,在日期类里面加上一条声明即可,此时在该函数体就可以使用对象中的私有成员变量。该声明不受类中访问限定符的限制。

📖重载>>

同理,>>也应该重载成全局的。

cpp 复制代码
istream& operator>> (istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

注意 :两个形参ind都不能用const修饰,前者是因为in本质上是一个对象,在进行流插入的时候,会改变对象里面的一些状态值,而后者是因为,我们就是希望通过流插入往d里面写入数据,所以也不能加const修饰。

小Tips :C++中的流插入和流提取可以完美的支持自定义类型的输入输出,而C语言的scanfprintf只能支持内置类型,这就是C++相较于C语言的一个优势。

四、const成员

将const修饰的成员函数称为const成员函数 ,const修饰类的成员函数,实际上修饰的是该成员函数隐含的*this,表明该成员函数中不能修改调用该函数的对象中的任何成员。这样一来,不仅普通对象可以调用该成员函数(权限的缩小),const对象也能调用该成员函数(权限的平移) 。经过const修饰的成员函数,它的形参this的类型就是:const T* const this

cpp 复制代码
bool Date::operator<(const Date& d) const//用const修饰
{
	if (_year < d._year)
	{
		return true;
	}
	else if (_year == d._year && _month < d._month)
	{
		return true;
	}
	else if (_year == d._year && _month < d._month && _day < d._day)
	{
		return true;
	}
	else
	{
		return false;
	}
}

对于所有的关系运算符重载函数,都应该加const修饰,因为它们不会改变对象本身。

📖总结:

并不是所有的成员函数都要加const修饰,要修改对象成员变量的函数,是不能加const修饰的,例如:重载的+=-=等,而成员函数中如果没有修改对象的成员变量,可以考虑加上const修饰,这样不仅普通对象可以调用该成员函数(权限的缩小),const对象也能调用该成员函数(权限的平移)。

五、取地址及const取地址操作符重载

cpp 复制代码
Date* operator&()
{
	cout << "Date* operator&()" << endl;
	return this;
}
const Date* operator&() const
{
	cout << "const Date* operator&() const" << endl;
	return this;
}

int main()
{
	Date d1(2023, 7, 22);
	const Date d2(2023, 7, 22);
	cout << &d1 << endl;
	cout << "--------" << endl;
	cout << &d2 << endl;
	return 0;
}

这俩取地址运算符重载函数,又构成函数重载,因为它们的默认形参this指针的类型不同,一个用const修饰了,另一个没有。const对象会去调用const修饰的取地址运算符重载函数。

小Tips :这两个&重载,属于类的默认成员函数,我们不写编译器会自动生成,所以这两个运算符重载一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。


🎁结语:

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,您的支持就是春人前进的动力!

相关推荐
智者知已应修善业7 分钟前
【51单片机按键调节占空比3位数码管显示】2023-8-24
c++·经验分享·笔记·算法·51单片机
韦禾水16 分钟前
记录一次项目部署到tomcat的异常
java·tomcat
曦月合一25 分钟前
树莓派安装jdk、tomcat、vnc、谷歌浏览器开机自启等环境配置
java·tomcat·树莓派
harder32138 分钟前
RMP模式的创新突破
开发语言·学习·ios·swift·策略模式
jinanwuhuaguo1 小时前
OpenClaw工程解剖——RAG、向量织构与“记忆宫殿”的索引拓扑学(第十三篇)
android·开发语言·人工智能·kotlin·拓扑学·openclaw
Rust研习社1 小时前
使用 Axum 构建高性能异步 Web 服务
开发语言·前端·网络·后端·http·rust
此剑之势丶愈斩愈烈1 小时前
openssl 自建证书
java
面汤放盐1 小时前
何时使用以及何时不应使用微服务:没有银弹
java·运维·云计算
0xDevNull1 小时前
Spring Boot 自动装配:从原理到实践
java·spring boot·后端