从零开始搞定类与对象(中)

发布于:2025-08-05 ⋅ 阅读:(14) ⋅ 点赞:(0)

运算符重载

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

 如代码,自定义类型进行使用“==”操作符会产生报错:

由于自定义类型比较复杂,所以默认情况下自定义类型是无法进行运算的,但是呢,有的自定义类型进行相关运算却是有意义的,比如我们经常遇到的日期类,两个日期相减得到的就是两个日期之间相隔的天数,日期加上天数就能得到另一个日期,学习运算符重载,我们可以让自定义类型进行运算,这样会更加方便。

话不多说,我们直接来看代码吧:

先写好日期类的代码,这是我们这一小节经常需要使用的类:

//日期类
#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()
	{
		cout << _year << '/' << _month << '/' << _day << endl;
	}
//在写运算符重载的算法中,我们不可避免的会用到类中的成员变量,所以需要将成员变量改为公有的,但其实我们一般不推荐这么做,在以后我们学习了友元函数就有更好地解决办法
//private:
	int _year;
	int _month;
	int _day;
};

现在,我们要对“==”运算符进行重载,也就是比较两个日期是否相同,按照运算符重载的特点:

//重载运算符名字:由operator和后面要定义的运算符共同构成
//具有返回值,参数,函数体
bool operator==(Date x1 ,Date x2)
{
    //将自定义类型的比较转化为成员变量中内置类型的比较
	return ((x1._year == x2._year) && (x1._month == x2._month) && (x1._day == x2._day));
}

写完这个代码后,再看是否还会有语法错误:

可以看到,当再次使用这个运算符时,就不会产生报错了。

另外,我们可以想一下上面那个运算符重载的代码还有啥可以改进的地方。我们在前面一节介绍过了,自定义类型的传值传参会调用拷贝构造,但是传引用传参就不会调用拷贝构造,所以在C++中,为了提高程序的性能,我们要习惯去使用引用传参:

//引用传参不需要调用拷贝构造
bool operator==(const Date& x1, const Date& x2)
{
	return ((x1._year == x2._year) && (x1._month == x2._month) && (x1._day == x2._day));
}

 那我们应当如何使用重载后的运算符呢?

方法一:函数调用的形式

bool ret= operator==( d1 , d2 );

方法二:就像内置类型一样直接使用运算符,一般情况下我们推荐这种写法

d1==d2;

现在我们就来运用一下这个运算符:

#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()
	{
		cout << _year << '/' << _month << '/' << _day << endl;
	}
	int _year;
	int _month;
	int _day;
};

//引用传参不需要调用拷贝构造
bool operator==(const Date& x1, const Date& x2)
{
	return ((x1._year == x2._year) && (x1._month == x2._month) && (x1._day == x2._day));
}
int main()
{
	Date d1(2025, 8, 3);
	Date d2(2025, 8, 8);
	if (d1 == d2)
	{
		cout << "两个日期相同" << endl;
	}
	else
	{
		cout << "两个日期不同" << endl;
	}
	return 0;
}

另外,为了在运算符重载中能够使用类里面的成员变量,我们将成员变量改为了公有的,但其实这种方式不太好,一种解决方法就是将运算符重载函数写到类里面去,我们来试一下:

#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& x1, const Date& x2)
	{
		return ((x1._year == x2._year) && (x1._month == x2._month) && (x1._day == x2._day));
	}
	void Print()
	{
		cout << _year << '/' << _month << '/' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;


};

//引用传参不需要调用拷贝构造

int main()
{
	Date d1(2025, 8, 3);
	Date d2(2025, 8, 8);

	return 0;
}

但是编译器会报错:

为啥就只是把运算符重载函数改到类里面就报错了呢?

这就是this指针在装神弄鬼了。表面上我们把函数写到类里面去是传递了两个参数,其实第一个参数的位置还有一个隐含的this指针。所以其实你是传递了3个参数的,但“==”只能接受2个操作数,所以会报错。

还需注意的是,在 C++ 中,当运算符重载函数作为成员函数定义在类内部时,this 指针指向的是运算符左侧的操作数对象的地址。这是运算符重载的核心规则之一。(这也就是第4点的意思)

所以,我们在类里面规定运算符重载时,我们自己写的参数个数应该比运算符实际能接受的操作数个数少一,比如,上面的代码应该改成:

#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
//编译器会把它处理为:operator(Date*this,const Date& x2)
	bool operator==(const Date& x2)
	{
		return ((_year == x2._year) && (_month == _month) && (_day == x2._day));
	}
	void Print()
	{
		cout << _year << '/' << _month << '/' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;


};

//引用传参不需要调用拷贝构造

int main()
{
	Date d1(2025, 8, 3);
	Date d2(2025, 8, 8);
	d1 == d2;
	//等价于:d1.operator==(d2);
	if (d1 == d2)
	{
		cout << "两个日期相同" << endl;
	}
	else
	{
		cout << "两个日期不同" << endl;
	}
	return 0;
}

 .*     ::  sizeof   ?:    . 5个运算符不能重载,我们现在来简单讲一下.*运算符的用法(这个运算符的挺少用的,简单看一下就好),见以下代码和注释:

//成员函数指针的创建与访问:
#include<iostream>
using namespace std;
void func1()
{
	cout << "void func1()" << endl;
}
class A
{
public:
	void func2()
	{
		cout << "void func2()" << endl;
	}
};

int main()
{
	//普通函数指针的创建:
	void (*pf1)() = func1;
	//利用函数指针来调用函数:
	(*pf1)();
	//类的成员函数指针的创建:
	void(A:: * pf2)() =& A::func2;
	//为啥类的成员函数指针要这样写:
	//在 C++ 中,类成员函数指针的声明和赋值需要特殊语法,这是由成员函数的本质特性决定的
	//成员函数(非静态)与普通函数不同,它隐含一个 this 指针参数(用于访问对象实例的数据)
	// 因此,成员函数指针的语法需要体现:所属类(A::),调用时的对象绑定(通过 .* 或 ->* 运算符)

	//成员函数指针的调用
//想一下,利用成员函数的指针调用成员函数可以这么调用嘛:(*pf2)();
	//不可以的:因为成员函数中是有隐含的this指针的,this指针接收的是调用函数的对象的地址,所以在调用成员函数
	//时,还需要指定对象
	A aa;
	//利用成员函数指针调用函数:
	(aa.*pf2)();//这就是.*运算符的用途
	return 0;
}

再来讲解一下特点8是啥意思:

重载操作符至少有一个类类型参数:意思是当你重载一个运算符(如 +==<< 等)时,至少有一个参数必须是自定义的类(class)或结构体(struct)类型,而不能全部是基本类型(如 intdoublechar 等)。

这是因为:

  • C++ 不允许你修改基本类型(如 intfloat 等)的运算符行为,否则会导致代码混乱。

  • 运算符重载的目的是为了让自定义类型(如 DateStringVector 等)也能像内置类型一样使用运算符。

 不能通过运算符重载改变内置类型对象的含义:意思是 你不能改变基本类型(如 intfloatchar 等)的运算符的原始行为。

这是因为:

  • 如果允许修改基本类型的运算符行为,比如让 1 + 1 返回 3,那代码会变得极其混乱,无法维护。

  • C++ 只允许你为自定义类型(如 DateString)定义新的运算符行为,而不能篡改内置类型的运算规则。

 赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象直接的拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。
 
赋值运算符重载的特点
 
1. 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成const 当前类类型引用,否则会传值传参会有拷贝
2. 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景。
3. 没有显式实现时,编译器会自动生成一个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
4. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载,也不需要我们显示实现MyQueue的赋值运算符重载。这里还有一个小技巧,如果一个类显示实现了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要。 

写代码来理解这些特点:

#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(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	//赋值运算符重载:如果是传值传参,在调用赋值运算符重载时还会调用拷贝构造
	//但这里是传引用传参,不会调用拷贝构造
	void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	void Print()
	{
		cout << _year << '/' << _month << '/' << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;


};

//引用传参不需要调用拷贝构造

int main()
{
	Date d1(2025, 8, 3);
//拷贝构造
	Date d2(d1);
	//赋值构造
	Date d3(2025, 7, 8);
	d3 = d1;
	d1.Print();
	d2.Print();
	d3.Print();
	return 0;
}

在调试过程中,确实调用了赋值运算符重载: 

 还需要区分一个点,如果在main函数中有这样一行代码:

Date d4 = d1;

请问这个代码是会调用拷贝构造还是赋值运算符重载?

答案是赋值运算符重载,可以看到d4这个对象是在创建的时候同时让对象d1对其进行初始化,这就是拷贝构造的另一种写法,可能容易与拷贝构造混淆,这一点在上一节我们也是讲到过的哦。

看到这里,就有一个注意的地方,我们上面写的赋值运算符重载是有一点小小的错误的哦,特点2已经告诉我们了,赋值运算符重载是有返回类型的哦,这样才能支持连续赋值。

那么我们可以想一下:如果有两个日期类对象d1和d3,我们要执行d1=d3,那么赋值运算符重载的返回值是什么呢?返回的应当是d1中的内容,因为是要把d3赋值给d1

	//执行:d1=d3
	//等价于:  d1.operator(d3)
	//形参部分:operator=(const Date& d)
	//实际上,编译器会将代码转化为:operator=(Date* this , const Date& d)
	//实参的传参部分:d1.operator(&d1 , d3 )
	//函数体内部:
	//{
	//   this->	_year = d._year;
	//   this->	_month = d._month;
	//	  this->_day = d._day;
	//}
	//最后返回值返回的应该是d1这个对象,而在参数中,d1这个对象的地址实际上传给了this,所以可以通过*this获得d1
//最终代码:
	Date& operator=(const Date& d)
	{
		_year =d._year;
		_month = d._month;
		_day = d._day;
		return *this;
	}
//为什么这里可以用引用返回
//原因一:因为*this并不是局部对象,*this得到的就是d1,他是在main函数中定义的,出了赋值运算符重载的作用域以后*this对应的空间并没有被销毁,所以这里可以传引用返回
//原因二:如果这里是传值返回,就要再调用拷贝构造函数,这样比较麻烦

看到这里,其实代码还可以再修改一下,想一下,假设有一种情况,有的小伙伴执行:d1=d1这种自己给自己赋值的代码(虽然这种代码无意义,但难免会有人真这么做),我们就可以把代码改成这样:

Date& operator=(const Date& d)
{
//当自己给自己复制时,this表示的是d1的地址,d是d1的别名,&d也就是&d1
//即this==&d1
      if(this!=&d)
  {
    	_year =d._year;
		_month = d._month;
		_day = d._day;
  }
		return *this;
}

日期类的实现

上面讲了这么多,我们就一起来实践一下,来完成日期类的实现吧!!!

//Date.h
#pragma once
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

class Date
{
public:
	//构造函数的声明
	Date(int year = 1900, int month = 1, int day = 1);
	//打印函数的声明
	void Print();
//日期的相关比较函数
	bool operator<(const Date& d);
	bool operator<=(const Date& d);
	bool operator>(const Date& d);
	bool operator>=(const Date& d);
	bool operator==(const Date& d);
	bool operator!=(const Date& d);

	// d1 += 天数
	Date& operator+=(int day);
	Date operator+(int day);
	// d1 -= 天数
	Date& operator-=(int day);
	Date operator-(int day);

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

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

	// d1++ -> d1.operator++(0)
	// 为了区分,构成重载,给后置++,强行增加了一个int形参
	// 这里不需要写形参名,因为接收值是多少不重要,也不需要用
	// 这个参数仅仅是为了跟前置++构成重载区分
	Date operator++(int);

	Date& operator--();
	Date operator--(int);
	//得到一月有多少天:
	int GetDayOfMonth(int year,int month)
	{
		int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if (month == 2 && ((year % 400 == 0) || (year % 4 == 0 && year % 100 != 0)))
		{
			arr[month]++;
		}
		return arr[month];
	}

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

//Date.cpp
#define  _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
//注意:全缺省类默认构造函数的缺省参数只在声明中写,不在定义中写
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

void Date::Print()
{
	cout << _year << '/' << _month << '/' << _day << endl;
}

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

bool Date::operator!=(const Date& d)
{
	return !(*this == d);
}
bool Date::operator>=(const Date& d)
{
	return !(*this < d);
}
bool Date::operator<=(const Date& d)
{
	return !(*this > d);
}
bool Date::operator< (const Date & d)
{
	return (_year < d._year) ||
		((_year == d._year) && (_month < d._month)) ||
		((_year == d._year) && (_month == d._month) && (_day < d._day));
}
bool Date::operator> (const Date& d)
{
	return (*this != d) && (!(*this < d));
}

Date& Date::operator+=(int day)
{
	_day += day;
	while (_day > GetDayOfMonth(_year, _month))
	{
		_day -= GetDayOfMonth(_year, _month);
		_month++;
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}
//不改变*this
Date Date::operator+(int day)
{
	Date d1 = *this;
	d1 += day;
	return d1;
}
//
//// d1 -= 天数
Date& Date:: operator-=(int day)
{
	if (_day > day)
	{
		_day -= day;
		return *this;
	}
	while (_day <= day)
	{
		_month--;
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		_day += GetDayOfMonth(_year, _month);
		if (_day > day)
		{
			_day -= day;
			break;
		}
	}
	return *this;
}
Date Date::operator-(int day)
{
	//Date d(*this);
	Date d = *this;
	d -= day;
	return d;
}
////后置加加:有拷贝构造
Date Date::operator++(int)
{
	//Date d(*this);
	Date d = *this;
	(*this) += 1;
	return d;
}
//前置--
Date& Date:: operator--()
{
	*this -= 1;
	return *this;
}
Date Date::operator--(int)
{
	//Date d(*this);
	Date d = *this;
	(*this) -= 1;
	return d;
}
//前置++:没有拷贝构造
Date& Date::operator++()
{
	*this += 1;
	return *this;
}
////
//// d1 - d2
int Date::operator-(const Date& d)
{
	//暴力搜索
	Date d1 = d;
	assert(*this > d);
	int count = 0;
	while (*this != d1)
	{
		d1++;
		count++;
	}
	return count;
}


// d1 += 100
//Date& Date::operator+=(int day)
//{
//	*this = *this + day;
//	return *this;
//}
//
//// d1 + 100
//Date Date::operator+(int day)
//{
//	Date tmp(*this);
//
//	tmp._day += day;
//	while (tmp._day > GetMonthDay(tmp._year, tmp._month))
//	{
//		tmp._day -= GetMonthDay(tmp._year, tmp._month);
//		++tmp._month;
//		if (tmp._month == 13)
//		{
//			++tmp._year;
//			tmp._month = 1;
//		}
//	}
//这是+和+=的另外一套写法,上面一种写法是让+去复用+=的逻辑,会经过2次复制拷贝
//这一种写法是让+=去复用+,会经过3次拷贝
//所以我们选用上一种方法:让+去复用+=



//test.cpp
#include"Date.h"
#include<iostream>
using namespace std;
int main()
{
	Date d1(2025, 1, 1);
	
	//Date d2(d1);
	//Date d3 = d2 + 100;
	//d3.Print();
	//d1 -= 100;
	//d1.Print();
	//Date d2 = d1 - 100;
	//d2.Print();
	/*++d1;*/
	//d1.Print();
	//Date d2=d1++;
	//d1.Print();
	//d2.Print();
	//Date d2=d1--;
	//d1.Print();
	//d2.Print();
	//Date d2=--d1;
	//d1.Print();
	//d2.Print();
	Date d2(2024, 9, 27);
	Date d3 = d2 + 96;
	d3.Print();
	int gap = d1 - d2;
	cout << gap << endl;
	return 0;
}


网站公告

今日签到

点亮在社区的每一天
去签到