您现在的位置是:首页 >学无止境 >【C++】类和对象(中)网站首页学无止境

【C++】类和对象(中)

不 良 2024-08-16 12:01:02
简介【C++】类和对象(中)

? 作者简介:一名在后端领域学习,并渴望能够学有所成的追梦人。
? 个人主页:不 良
? 系列专栏:?C++  ?剑指 Offer  
? 学习格言:博观而约取,厚积而薄发
? 欢迎进来的小伙伴,如果小伙伴们在学习的过程中,发现有需要纠正的地方,烦请指正,希望能够与诸君一同成长! ?


类的6个默认成员函数

在C语言中,我们在某些特定情况下需要进行初始化和销毁操作,但是有的时候会因为自己的疏忽造成程序出现错误,出现内存泄漏等问题。在C++中可以通过编译器自动调用默认成员函数自动进行初始化和销毁空间。

如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数(构造函数、析构函数、拷贝构造、赋值重载、取地址及const取地址操作符重载)。当我们不写时,编译器自动生成;当我们自己定义了之后,编译器就不生成了。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

class Date {};

image-20230530093610011

构造函数

构造函数概念

对于以下Date类:

#include <iostream>
using namespace std;
class Date {
public:
    //初始化函数
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    //打印函数
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;//实例化对象
	d1.Init(2022,5,30);//初始化对象
	d1.Print();//打印结果为2022/5/30

	Date d2;//实例化
	d2.Init(2023, 5, 30);//初始化对象
	d2.Print();//打印结果为2023/5/30
	return 0;
}

对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。

我们通过在Date类中自己定义一个构造函数就可以实现在实例化对象时自动将信息设置进入,代码如下:

#include <iostream>
using namespace std;
class Date {
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2022, 5, 30);
	d1.Print();//打印结果为2022/5/30

	Date d2(2023, 5, 30);
	d2.Print();//打印结果为2023/5/30

	return 0;
}

那上面的代码中我们是否可以理解成构造函数的使用就是d1.Date(2022,5,30)

不能,因为这里的时候d1还没有被实例化出来,构造函数不能用对象去调用。但是如果我们等对象实例化出来之后,即Date d1;d1.Date(2022,5,30);如果这样写,和我们刚开始用Init函数进行初始化没有区别,所以并不能这样理解。

构造函数特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

构造函数的特征:

1.函数名和类名相同;

2.无返回值,这里的无返回值并不是指返回值为void,而是真的无返回值,如上面Date类的构造函数就是Date(){}

3.对象实例化时编译器自动调用对应的构造函数;

4.构造函数可以重载,也就是说一个类可以有多个构造函数,有多种初始化方式;

#include <iostream>
using namespace std;
class Date {
public:
	//1.无参构造函数
	Date()
	{}
	//2.带参构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1; //调用无参构造函数
	//通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明,
	// 如果加了括号编译器就无法识别是函数声明还是调用构造函数
	// 如下面这行代码就是d3函数的声明,该函数无参,返回一个日期类型的对象
	//Date d3(); 

	Date d2(2023, 5, 30);//调用带参的构造函数

	return 0;
}

通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明,如果加了括号编译器就无法识别是函数声明还是调用构造函数,如Date d3();代码就是d3函数的声明,该函数无参,返回一个日期类型的对象;如果是有参数的构造函数则可以这样写Date d4(2023,5,30);,因为如果是函数声明的话要带参数类型,即Date d4(int year,int month,int day)

注意:无参的构造函数、全缺省的构造函数以及用户不写编译器自动生成的构造函数被称为默认构造函数。默认构造函数只能有一个,也就是说无参的构造函数和全缺省的构造函数不能同时存在,否则编译器会报错。

//无参构造函数和全缺省的构造函数不能同时存在,因为默认构造函数只能存在一个

//无参构造函数
Date()
{}

//全缺省的构造函数
Date(int year = 2022, int month = 5, int day = 30)
{
    _year = year;
    _month = month;
    _day = day;
}

5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

#include <iostream>
using namespace std;
class Date {
public:
	//我们自己实现的构造函数
	/*Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
    //当我们将自己实现的构造函数注释掉时,编译器生成默认的构造函数,此时Date d1;代码可以编译通过
    //当我们将自己实现的构造函数取消注释之后,此时编译器不会生成默认的构造函数,Date d1;代码编译失败,必须要传参,使用代码Date d2(2023, 5, 30);可以编译通过
	Date d1;
	
	//Date d2(2023, 5, 30);//调用带参的构造函数

	return 0;
}

那么,既然在我们不实现构造函数的情况下,编译器会自动生成默认的构造函数,那么我们为什么要自己实现构造函数呢?还是以Date类为例:当我们不自己实现构造函数时,使用编译器默认生成的构造函数会出现随机值的情况,具体如下:

#include <iostream>
using namespace std;
class Date {
public:
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	d1.Print();
	return 0;
}

打印结果为:

image-20230530164048319

可见,编译器默认生成的构造函数打印结果随机值,那编译器默认生成的构造函数意义在哪?

C++把类型分为了内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如int/char/double……;自定义类型就是使用struct/class/union等自己定义的类型。

编译器自己生成的默认构造函数有以下处理机制:

  • 编译器自动生成的构造函数对内置类型成员不做处理;

  • 对于自定义类型,编译器会再去调用它们自己的默认构造函数。

我们通过下面的程序来观察,下面的程序中定义了一个Time类和一个Date类,Date类中定义了一个Time类型的成员变量:

class Time
{
public:
	Time()
	{
		cout << "Time类的构造函数" << endl;
	}
};
class Date
{
private:
    //C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

打印结果:image-20230602154751284

通过打印结果我们可以发现在Date类中对于自定义类型Time类型会去调用Time类的默认构造函数。

当自定义类型变量和内置类型混着使用的时候,并不会对内置类型进行处理,我们可以考虑给内置类型设置缺省值。

C++11 中针对内置类型成员变量不初始化的缺陷,规定内置类型成员变量在类中声明时可以给默认值。当我们没有显示初始化的时候,使用成员变量的缺省值。

6.无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个(调用时会存在歧义,建议留全缺省的)。

注意:无参构造函数、全缺省构造函数、用户没写编译器默认生成的构造函数,都可以认为是默认构造函数。

下面程序就是同时定义了无参构造函数和全缺省构造函数,程序会报错。

#include <iostream>
using namespace std;
class Date {
public:
	//无参构造函数
	Date()
	{}

	//全缺省的构造函数
	Date(int year = 2022, int month = 5, int day = 30)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1; //会报错,对重载函数的调用不明确
	return 0;
}

析构函数

析构函数概念

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

如下面的Date类:

class Date
{
public:
	Date()
	{
		cout << "Date()" << endl;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	return 0;
}

当一个类对象销毁时,其中的局部变量也会随着该对象的销毁而销毁,如Date类,当我们实例化一个对象d1并销毁时,d1当中的局部变量_year/_month/_day也会被编译器销毁,局部对象销毁工作是由编译器完成的。

Date类这样的类是不需要析构函数的,因为它内部没有什么资源需要清理,但是像栈这种涉及到申请空间资源,需要自己定义析构函数释放资源。

析构函数特性

1.析构函数的函数名是在类名前加上字符~

class Date
{
public:
	~Date()
	{
		cout << "~Date()" << endl;
	}
};

2.析构函数无参数,无返回值类型。

这里的无返回值就是没有返回值,并不是指返回值类型为void

3.一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。

注意:析构函数不能重载。

编译器自动生成的析构函数的处理机制:

  • 编译器自动生成的析构函数对内置类型成员不做处理;
  • 对于自定义类型,编译器会再去调用它们自己的默认析构函数。
class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
	}
	~Time()
	{
		cout << "~Time()" << endl;
	}
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

程序运行输出结果:

image-20230602170218459

在此程序中main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的构造函数和析构函数?

因为main方法中创建了Date类型对象d,而d中包含4个成员变量,其中_year, _month,_day三个是内置类型成员,默认构造函数和默认析构函数对内置不做处理,内置类型成员变量销毁时不需要资源清理,最后系统直接将其内存回收即可;而_tTime类对象,实例化时调用Time类的默认构造函数。在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供析构函数,则编译器会给Date类生成一个默认的析构函数,目的是在其内部调用Time类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数。
总结:创建哪个类的对象则调用该类的构造函数,销毁那个类的对象则调用该类的析构函数。

4.对象生命周期结束时,C++编译器会自动调用析构函数

5.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。

6.先构造的后析构,后构造的先析构。

因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合栈先进后出的原则。

拷贝构造函数

拷贝构造函数概念

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

const参数做形参很多好处:传参的时候可以传不是const类型的参数,const类型的参数也可以传递,属于权限的缩小或者平移;不加const可能会造成权限的放大,还有因为顺序错误,改变引用对象的值。

class Date {
public:
	//构造函数
	Date(int year = 2023, int month = 6, int day = 2)
	{
		cout << "Date()" << endl;
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	//打印函数
	void Print()
	{
		cout << _year << "/" << _month << "/"  << _day << endl;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
int main()
{
	Date d1(2022, 5, 6);
	d1.Print();

	Date d2(d1);
    //也可以使用=进行拷贝构造
    //Date d2 = d1
	d2.Print();
	return 0;
}

打印结果:

image-20230602173218685

实例化d1时调用构造函数,然后打印;实施化d2时调用拷贝构造函数,然后打印,然后析构d1d2两个对象。

拷贝构造函数表示方式有Date d2(d1)/Date d2 = d1

拷贝构造函数特性

拷贝构造函数也是特殊的成员函数,其特征如下:

1.拷贝构造函数是构造函数的一个重载形式。

2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

通过前面的学习我们知道函数传值传参就是将实参内容拷贝一份给形参;引用传参中形参是实参的别名。

普通的函数类型可以直接传值传递,但是自定义类型不能随便拷贝,自定义类型的拷贝要调用拷贝构造函数。

为什么自定义类型的函数传参不能使用传值传参呢?

因为有些场景下传值拷贝会出错。对于日期类可以直接使用传值拷贝;但是对于栈这样的类,不能直接拷贝,如果按字节拷贝,那么两个指针指向同一个空间,先后调用析构函数将会对同一块空间析构两次,同时当他们push的时候,也是对同一块空间进行操作。

浅拷贝:编译器能干的事情就是浅拷贝,一个字节一个字节的拷贝。

image-20230603160626492

所以对于栈这样的类型,要进行深拷贝的拷贝构造,即自己去开一个新的空间。所以规定自定义类型的拷贝要调用拷贝构造,避免出现指向同一块空间及多次析构的情况。

所以使用传值传参的时候要调用拷贝构造,使用引用传参的时候不用调用拷贝构造。

如果拷贝构造函数使用传值传参,使用传值传参的时候要调用拷贝构造,而调用拷贝构造函数又需要先传参……如此循环最终就引发了无穷递归。示例图如下:

为避免如下无穷递归的情况,所以规定拷贝构造函数不能使用传值传参,要使用引用传参。

自定义类型的对象进行函数传参时,一般推荐使用引用传参。使用传值传参也可以,但是每次传参时都要调用拷贝构造函数。

3.若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝或者值拷贝。

如下面的Date类,我们并没有自己实现拷贝构造函数,是由编译器自动生成的拷贝构造函数完成对象的拷贝。

class Date {
public:
	//构造函数
	Date(int year = 2023, int month = 6, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//打印函数
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
int main()
{
	Date d1(2022, 5, 6);
	d1.Print();

	Date d2(d1);
	d2.Print();
	return 0;
}

打印结果:

image-20230603162411691

编译器自动生成的拷贝构造函数机制:

1.编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝(值拷贝)。

2.对于自定义类型,编译器会再去调用它们自己的默认构造函数。

上面的Date类已经验证了编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝(值拷贝),我们再来通过下面的程序验证对于自定义类型,编译器会再去调用它们自己的默认构造函数:

class Time {
public:
	Time()
	{
		_hour = 17;
		_minute = 4;
		_second = 53;
	}
	Time(const Time& t)
	{
		cout << "Time(const Time& t)" << endl;
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

class Date {
private:
	//内置类型
	int _year = 1;
	int _month = 1;
	int _day = 1;
	//自定义类型
	Time _t;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

上面程序中用已经存在的d1拷贝构造d2,此时会调用Date类的拷贝构造函数;但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构造函数,在Date类中,Time类型的_t是自定义类型,调用Time类自己的拷贝构造函数。

在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其对应的拷贝构造函数完成拷贝的。

4.编译器自动生成的拷贝构造函数不能实现深拷贝。

编译器生成的默认拷贝构造函数会对内置类型完成浅拷贝。因此对于下面的Date类,浅拷贝实际上就是将d1的内容复制了一份拷贝给d2,所以浅拷贝也被叫做值拷贝。

class Date {
private:
	//内置类型
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

但是编译器生成的拷贝构造函数并不能完成深拷贝,所以在某些场景下,我们不能依赖于编译器生成的拷贝构造函数。如下面的代码我们实际只想利用栈s1创建一个新的栈对象s2

class Stack {
public:
	Stack(int capacity = 4)
	{
		int* p = (int*)malloc(sizeof(int) * capacity);
		if (p == nullptr)
		{
			exit(0);
		}
		_a = p;
		_top = 0;
		_capacity = capacity;
	}
    //析构函数,如果取消注释则直接报错,原因就是对同一个空间析构两次
	/*~Stack()
	{
		free(_a);
	}*/
	void Print()
	{
		cout << _a << endl;
	}
private:
	int* _a;
	int _top;
	int _capacity = 4;
};
int main()
{
	Stack s1;
	s1.Print();

	Stack s2(s1);
	s2.Print();
	return 0;
}

打印结果如下:打印出来s1s2的地址相同,不符合我们的预期。

image-20230603173632343

如果使用编译器自动生成的拷贝构造函数,完成的是浅拷贝,拷贝完成之后指向的是同一块空间:

指向同一块空间存在的问题:

1.插入删除数据的时候会造成覆盖,因为top1的改变不引起top2的改变,指针指向的空间相同,所以会引起数据的覆盖;

2.析构两次(先构造的后析构,后构造的先析构,s2先析构,s1再析构),程序崩溃。

这也就意味着如果我们对其中任意一个进行操作都会影响另外一个,但是我们希望的是两个栈之间互不影响。默认的拷贝我们都叫做值拷贝或者浅拷贝,深拷贝做更深一层的拷贝,就是让他们各自有独立的空间。所以我们要进行深拷贝以达到它们拥有各自的空间。

image-20230603174844008

class Stack {
public:
    //构造函数
	Stack(int capacity = 4)
	{
		int* p = (int*)malloc(sizeof(int) * capacity);
		if (p == nullptr)
		{
			exit(0);
		}
		_a = p;
		_top = 0;
		_capacity = capacity;
	}
    //拷贝构造函数
	Stack(const Stack& s)
	{
		int* _arr = (int*)malloc(sizeof(int) * s._capacity);
		if (_arr == nullptr)
		{
			perror("malloc failed");
			exit(0);
		}
		_a = _arr;
		_top = s._top;
		_capacity = s._capacity;
        //memcpy函数
		memcpy(_a, s._a, sizeof(int) * s._top);

	}
	~Stack()
	{
		free(_a);
	}
	void Print()
	{
		cout << _a << endl;
	}
private:
	int* _a;
	int _top;
	int _capacity = 4;
};
int main()
{
	Stack s1;
	s1.Print();

	Stack s2(s1);
	s2.Print();
	return 0;
}

类中如果没有涉及资源申请时,拷贝构造函数写不写都可以;但是一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

5.拷贝构造函数典型调用场景:

使用下面的类,改变main函数来观察调用场景:

class Date {
public:
	//构造函数
	Date(int year = 2023, int month = 6, int day = 2)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//拷贝构造函数
	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year = 1;
	int _month = 1;
	int _day = 1;
};
void func1(Date d)
{
}
Date func2(Date d)
{
	return d;
}
  • 使用已存在对象创建新对象
int main()
{
	Date d1(2022, 5, 6);
	//使用已存在的对象创建新对象时调用拷贝构造函数
	Date d2(d1);
	return 0;
}

输出结果:

  • 函数参数类型为类类型对象

下面的程序中func1函数函数参数为类类型对象,此时调用func1函数要传值传参,所以要调用拷贝构造函数。

void func1(Date d)
{
}
int main()
{
	Date d1(2022, 5, 6);
	//func1函数参数类型为类类型对象
	func1(d1);
	return 0;
}

输出结果:

image-20230603195258324

  • 函数返回值类型为类类型对象

函数返回值类型为类类型对象时要先将return的值临时拷贝到上一层栈帧中的临时变量中,所以要调用拷贝构造函数。本例中就是要将d拷贝一份,所以要调用拷贝构造函数,函数参数类型也为类类型对象,所以要调用两次拷贝构造函数。

Date func2(Date d)
{
	return d;
}
int main()
{
	Date d1(2022, 5, 6);
	func2(d1);
	return 0;
}

输出结果:

image-20230603195435249

为了提高程序效率减少拷贝,一般自定义类型对象传参时,尽量使用引用类型;返回时根据实际场景,能用引用尽量使用引用。

赋值运算符重载

运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似

d1 == d2;//可读性强,书写简单
IsSame(d1,d2);//可读性差,书写麻烦

函数名字为:关键字operator后面接需要重载的运算符符号。如赋值运算符重载函数名为operator==

函数原型:返回值类型 operator操作符(参数列表)。如重载赋值运算符函数原型为bool operator== (const Date& d1,const Date& d2)

注意:
1.不能通过连接其他符号来创建新的操作符:比如operator@。
2.重载操作符必须有一个类类型或枚举类型的操作数。
3.用于内置类型的操作符,重载后其含义不能改变。
4.作为类成员的重载函数时,函数有一个默认的形参this,限定为第一个形参。
5.sizeof :: .* ?: . 这5个运算符不能重载。

我们通过重载==操作符来熟悉一下:

由上面第4点我们知道当运算符重载作为类的成员函数时,函数有一个默认的形参this,限定为第一个形参,也就是说,当是类的成员函数时,函数参数列表中需要我们写的只有右操作数,左操作数为this指针(a == b,其中a为左操作数,b为右操作数)。

class Date {
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//==运算符重载
	bool operator==(const Date& d)
	{
		return _year == d._year 
			&& _month == d._month 
			&& _day == d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};

我们也可以将该运算符重载函数放在类的外面,在类外没有this指针,所以此时函数的形参设置为两个。

但因为此时类中的成员变量为私有,外部无法访问,我们可以将类中的成员变量设置为公有的(public),外部就可以访问该类的成员变量了(也可以使用用友元函数解决)。

class Date {
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	
	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;
}

赋值运算符重载

1.赋值运算符重载格式

  • 参数类型:const T&,传递引用可以提高传参效率

赋值运算符只能是成员函数,因为赋值运算符重载就算我们不实现,编译器也会默认自动生成,所以赋值运算符重载函数的第一个形参默认是this指针,第二个形参是赋值运算符的右操作数。
由于是自定义类型传参,我们若是使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数(即右操作数)最好使用引用传参(第一个参数是默认的this指针),并且在函数体内不会对右操作数进行修改,所以最好加上const进行修饰。

  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

如果只以d2 = d1这种方式使用赋值运算符,赋值运算符重载函数就不用有返回值,因为在函数体内已经通过this指针对d2进行修改。但是为了支持连续赋值,即d3 = d2 = d1,我们就需要为函数设置一个返回值,返回值应该是赋值运算符的左操作数,即*this

为了提高效率避免调用拷贝构造,最好使用引用返回,因为this指针指向的对象出了函数作用域并没被销毁,所以可以使用引用返回。

  • 检测是否自己给自己赋值

若是出现d1 = d1这种情况,不必进行赋值操作,因为是没有意义的,所以先判断是不是自己给自己赋值。

  • 返回*this :要符合连续赋值

为了支持连续赋值,我们返回的时候返回赋值运算符的左操作数,而作为类成员函数只能通过this指针访问到左操作数,所以要返回左操作数就只能返回*this

class Date
{
public:
	Date(int year = 2023, int month = 6, int day = 3)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	Date& operator=(const Date& d)
	{
		//检测是否自己给自己赋值
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		//返回*this,要符合连续赋值
        //d3 = d2 = d1时,要返回的是d2,即*this
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};

2.赋值运算符只能重载成类的成员函数不能重载成全局函数

class Date
{
public:
	Date(int year = 2023, int month = 6, int day = 3)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	int _year;
	int _month;
	int _day;
};
// 赋值运算符重载成全局函数,全局函数没有this指针需要两个参数
Date& operator=(Date& left, const Date& right)
{
	if (&left != &right)
	{
		left._year = right._year;
		left._month = right._month;
		left._day = right._day;
	}
	return left;
}
// 编译失败:
//“operator =”必须是非静态成员

原因:赋值运算符也是类的6个默认成员函数之一,默认成员函数即我们不自己实现编译器也会自动生成。所以赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突,编译失败。故赋值运算符重载只能是类的成员函数。

3.一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝

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

class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
    //Time类的赋值运算符重载
	Time& operator=(const Time& t)
	{
		cout << "Time& operator=(const Time& t)" << endl;
		if (this != &t)
		{
			_hour = t._hour;
			_minute = t._minute;
			_second = t._second;
		}
		return *this;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	Date d2;
	d1 = d2;
	return 0;
}

打印结果:

image-20230603211522630

尽管编译器会自动生成赋值运算符重载函数,但是并不代表我们可以不用实现,如果类中未涉及到资源管理,赋值运算符是否实现都可以;但是一旦涉及到资源管理则必须要实现。以下代码就涉及到资源管理:

class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (int*)malloc(capacity * sizeof(int));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
	void Push(const int& data)
	{
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	int* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2;
	s2 = s1;
	//析构时候析构两次,程序报错
	return 0;
}

s1中压入1、2、3、4,然后实例化s2s2中空间大小为10并没有存储元素,但是Stack类中并未自己实现赋值运算符重载是由编译器自动生成的赋值运算符重载,所以当s2 = s1的时候完成的是字节序拷贝,直接将s1中的全部成员变量的值给s2的成员变量,此时s2也指向s1的那块空间,s2原来的那部分空间丢失了,存在内存泄漏;s1s2共享同一份内存空间,最后销毁时会导致同一份内存空间释放两次而引起程序崩溃。

观察以下代码调用的是拷贝构造函数还是赋值运算符重载函数?

Date d1(2021, 6, 1);//代码1 构造函数
Date d2(d1);//代码2 拷贝构造
Date d3 = d1;//代码3 拷贝构造
Date d4;
d4 = d1;//代码4  赋值运算符重载

代码2和代码3调用的都是拷贝构造函数;代码4调用的是赋值运算符重载函数。

拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。

const成员

const修饰的成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

如果成员函数声明和定义分离,都要在函数后加上const

image-20230604154902854

成员函数中原本的this类型为类类型* const this,this指针本身不能被改变,但是指针指向的内容可以被改变,但是如果变为const成员函数之后,this指针类型为变为了const 类类型* const this,指针本身和指针指向的内容都不能被改变了。

思考下面几个问题(面试题):

1.const对象可以调用非const成员函数吗?

2.非const对象可以调用const成员函数吗?

3.const成员函数内可以调用其他的非const成员函数吗?

4.非const成员函数内可以调用其他的const成员函数吗?

解答:

1.不可以,const对象只能调用const成员函数,const对象的内容不能被修改,即this指针被const所修饰,然而非const成员函数的this指针没有被const所修饰,如果const对象调用非const成员函数就属于权限的放大,权限只能平移或者缩小,所以不可以调用。

2.可以,非const对象的this指针没有被const修饰,调用const成员函数属于权限的缩小。

3.不可以,const成员函数内的this指针已经被const修饰成const *this,再调用非const函数就属于是权限的放大,所以已经不能再调用。

4.可以,非const成员函数内的this指针还没有被const修饰,调用其他的const成员函数属于权限的缩小,所以可以调用。

总结:

const 对象不可以调用非 const 成员函数 (权限放大);非 const 对象可以调用const成员函数 (权限缩小)。

const 成员函数内不可以调用其它非 const 成员函数;非const成员函数内可以调用其它const成员函数。

成员函数加 const,变成const成员函数, const 对象可以调用,非const对象也可以调用。

class Date
{
public:
	Date(int year = 2023, int month = 6, int day = 4)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//Print函数构成重载函数

	void Print()//void Print(Date* const this)
	{
		cout << "非const成员函数" << endl;
		cout << _year << "/" << _month << "/" << _day << endl;
	}

	void Print() const//void Print(const Date* const this)
	{
		cout << "const成员函数" << endl;
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023, 6, 4);
	//d1是非const对象调用非const成员函数
	d1.Print();
	
	//d2是const对象调用const成员函数
	const Date d2(2023, 10, 1);
	d2.Print();

	return 0;
}

Print函数构成重载,分别是非const成员函数和const成员函数,d1是非const对象调用非const成员函数,d2const对象调用const成员函数,当只有const成员函数的时候非const对象也可以调用const成员函数。

打印结果如下:

image-20230604170452658

不是说所有的成员函数都要加 const ,具体要看成员函数的功能,如果成员函数是修改型 (operrato+=、Push),那就不能加;如果是只读型 (Print、operator==),那就最好加。

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

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。

class Date
{
public:
	Date* operator&()
	{
		return this;
	}
	const Date* operator&()const
	{
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1;
	cout << &d1 << endl;//输出d1的地址
	return 0;
}

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。

日期类的实现

定义类时,如果函数声明和定义分离,函数声明时可以带缺省参数,函数定义时不能带缺省参数,否则会报错。

普通函数声明和定义分离时,无论声明还是定义都可以带缺省参数,但是声明和定义中不能同时出现。

日期类中我们要实现的功能函数声明和解释如下:

//Date.h头文件
//函数声明中可以带缺省参数,函数定义和声明分离,定义中不能带缺省参数
class Date{
public:
    //构造函数,使用全缺省的默认构造函数
    Date(int year = 2023, int month = 6, int day = 4);
    //日期 += 天数
    Date& operator+=(int day);
    //日期 + 天数,
    //不改变this指针指向的内容,所以使用const修饰,后面的函数中只要是不改变*this内容都使用const修饰,使其称为const成员函数
    Date operator+(int day) const;
    //日期-=天数
    Date& operator-=(int day);
    //日期-天数
    Date operator-(int day) const;
    //打印日期函数
    void Print() const ;
    //前置++
    Date& operator++();
    //后置++,当要实现后置++重载时,给定一个int类型的参数,没有其他实际意义,只是为了构成重载。
    Date operator++(int);
    //前置--
    Date& operator--();
   	//后置--
    Date operator--(int);
    //日期类的大小关系比较
    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;
    //日期-日期 
    int operator-(const Date& d) const;
    //因为日期类里面不涉及资源的申请和释放所以拷贝构造函数、赋值运算符重载函数和析构函数使用编译器自动生成的就可以,不用再对其实现。
};
//流插入 >> 
//流输出 <<

构造函数

构造函数主要实现的就是日期的赋值操作,所以我们要先检查日期的合法性。

在类外实现的时候要标明类域。

// 获取本月天数
//使用内联函数,提高效率
inline int GetMonthDay(int year,int month)
{
	//使用数组存储每个月的天数
	int monthday[] = {0, 31,28,31,30,31,30,31,31,30,31,30,31 };
	//注意闰年2月天数为29天,闰年:能被4整除且不能被100整除或者能被400整除
	//判断是否是闰年
	if ((year % 4 == 0 && year % 100 == 0) || (year % 400 == 0))
	{
		//判断是否是2月
		if (month == 2)
		{
			//如果是闰年2月+1就是29天
			return monthday[month] + 1;
		}
	}
	//不是直接返回当月天数
	return monthday[month];
}
Date::Date(int year, int month, int day)
{
	//先判断年是否合法
	if (year >= 0
		&& month >= 1 && month < 13
		&& day >= 1 && day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "日期非法" << endl;
		exit(-1);//直接退出程序
	}
}

GetMonthDay函数可能被多次调用,最好将其设置为内联函数;
GetMonthDay函数中存储每月天数的数组最好是用static修饰,存储在静态区,避免每次调用该函数都需要重新开辟数组。

日期 += 天数

先判断天数day是正数还是负数,如果是负数,复用-=运算符;如果是正数,我们将天数加到当前Date对象的_day上,然后判断日期是否合法,若不合法不断调整直到日期合法为止。

调整方法:判断_day是否合法,如果不合法则减去当前月的天数,_month加1;如果_month不合法,则将_year加1,将_month置为1。如此不断调整,直到日期合法为止。

实现+=运算符重载要支持这样写d1 = d3 += 100,即+=要支持连续赋值,所以要有返回值。出了作用域,this指针还在所以可以用引用返回。

注意不能因为要引用返回,而将tmp设置为静态变量,因为每次改变都会影响``tmp`,而静态变量只会初始化一次。

//日期 += 天数
Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		//复用-=运算符重载函数
		*this -= -day;
	}
	else
	{
		_day = day + _day;
		//通过while循环判断日期是否合法
		while (_day > GetMonthDay(_year, _month))
		{
			_day -= GetMonthDay(_year, _month);
			//注意先month++再判断
			_month++;
			if (_month > 13)	
			{
				_month = 1;
				_year++;
			}
			
		}
	}
	return *this;
}

日期 + 天数

+运算符的重载我们可以通过复用+=重载运算符,而且+运算符不会修改原本的内容,所以我们可以用const修饰成员函数。

//+不改变原本内容
Date Date::operator+(int day) const
{
	Date tmp(*this);//调用拷贝构造
	//const成员函数不能修改this指针指向的内容
	//*this += day;
	
	//复用+=
	tmp += day;
	return tmp;
}

扩展:

我们可以实现+=运算符重载,实现+运算符重载时复用+=;也可以实现+运算符重载,实现+=运算符重载时复用+;那么哪种方法更好呢?

我们实现+复用+=程序如下:

Date Date::operator+(int day) const
{
	Date tmp(*this);
	if (_day < 0)
	{
		//复用-=运算符重载
		tmp -= - day;
	}
	else
	{
		tmp._day += day;
		while (tmp._day > GetMonthDay(tmp._year,tmp._month))
		{
			tmp._day -= GetMonthDay(tmp._year, tmp._month);
			//注意先month++再判断
			tmp._month++;
			if (tmp._month > 12)
			{
				tmp._year++;
				tmp._month = 1;
			}
		}
	}
	Date tmp(*this);
}
Date& Date::operator+=(int day)
{
	*this = *this + day;
	return *this;
}

通过上面的程序我们可以看到在实现+运算符重载时,Date tmp(*this);这里一次拷贝构造;因为是传值返回,所以Date tmp(*this);还有一次拷贝构造,再实现+=复用+时,还有两次拷贝构造和*this = *this + day这里的一次赋值。

但是我们用+=实现+时,+=运算符重载函数本身没有发生任何拷贝构造行为,复用+=实现+运算符时有两次拷贝构造和一次赋值。

所以相比较而言,实现+运算符时复用+=运算符这种方式比较好。

日期 -= 天数

实现-=运算符时需要加上上一个月的天数。同时返回类型使用引用,实现支持连续-=

//日期-=天数
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
        //复用+=
		*this += -day;
	}
	else
	{
		_day -= day;
		while (_day <= 0)
		{
			--_month;
			if (_month == 0)
			{
				_year--;
				_month = 12;
			}
			_day += GetMonthDay(_year, _month);
		}
	}
	return *this;
}

日期 - 天数

和实现+=+运算符相同,通过复用-=实现。

//日期-天数
Date Date::operator-(int day) const
{
	Date tmp(*this);
	tmp -= day;
	return tmp;
}

打印日期函数

//打印日期函数
void Date::Print() const
{
	cout << _year << "/" << _month << "/" << _day << endl;
}

前置++

前置++返回的是*this,出了函数作用域不会被销毁,所以可以使用引用返回。

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

后置++

由于前置++和后置++的运算符均为++,且操作数都是只有一个,为了方便区分我们给后置++的运算符重载的参数加上一个int类型参数,使用后置++时不需要给这个int参数传入实参,这里int参数的作用只是为了跟前置++构成函数重载。

后置++需要返回被加之前的值,所以先用对象tmp保存被加之前的值,然后再复用+=将对象加一,最后返回tmp对象。由于tmp对象是局部对象,出了该函数作用域就被销毁了,所以后置++只能使用传值返回。

//后置++要给定一个int类型的参数,没有其他实际意义,只是为了构成重载
Date Date::operator++(int)
{
	Date tmp(*this);
	//复用+=
	*this += 1;
	return tmp;
}

前置–

前置--和后置--原理和前置++、后置++一样。

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

后置–

//后置--要给定一个int类型的参数,没有其他实际意义,只是为了构成重载
Date Date::operator--(int)
{
	Date tmp(*this);
	//复用+=
	*this -= 1;
	return tmp;
}

日期类的大小关系比较

日期类的大小关系比较我们可以通过实现其中的两个,其他的通过复用来实现。

进行日期的大小比较,我们并不会改变传入对象的值,所以可以将其定义为const成员函数。

>运算符重载

bool Date::operator>(const Date& d) const
{
	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) const
{
	return (_year == d._year && _month == d._month && _day == d._day);
}

>=运算符重载

复用>重载运算符和==重载运算符。

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 !(*this == d);
}

日期 - 日期

日期-日期算出来的是两个日期之间相差的天数。

需要让较小日期的天数一直加一,直到最后和较大的日期相等即可,这个过程中较小日期所加的总天数便是这两个日期之间差值的绝对值。设置flag用于判断是哪一个日期大哪一个日期小。若是第一个日期大于第二个日期,则返回这个差值的正值,若第一个日期小于第二个日期,则返回这个差值的负值。

下面的代码中复用了许多重载的运算符,所以不需要我们考虑底层实现,直接使用就可以。

//日期-日期 
//下面的代码中复用了许多重载的运算符,所以不需要我们考虑底层实现,直接使用就可以。
int Date::operator-(const Date& d) const
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (*this < d) //复用<
	{
		min = *this;
		max = d;
		flag = -1;
	}
	int n = 0;//用于记录加了多少次1
	while (min != max ) //复用!=
	{
		++min;//复用++
		++n;
	}
	return n * flag;
}

流插入(<<)运算符重载

我们平时喜欢用流插入<<,其实流插入本身也是一个库里面的重载运算符,因此流插入能够自动识别类型;cincout就是一个类对象,cout是一个ostream的全局类对象,cin是一个istream的全局类对象:

image-20230605162444310

在我们自己定义的类中除了赋值运算符能够直接使用外,其他的都需要重载。

我们可以自己实现流插入,而不用每次都依赖Print函数。即实现下面的程序:

//流插入<<运算符重载
inline ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << "/" << d._month << "/" << d._day << endl;
    return out;
}

out就是cout的别名,类型是ostream类型。但是如果我们将其实现时定义为Date类的成员函数,那么默认第一个参数就是this,即Date类型,右操作数才是ostream类型,如果我们要将其实现为成员函数,我们使用时就要d1 << cout这样写才能调用,和我们之前经常使用的形式不一样,所以我们可以写在外面,使其成为全局函数。

但是如果实现成为全局函数,访问不了类中私有的成员变量,此时我们可以通过在类中定义函数获取成员变量或者将函数定义为友元函数。

//友元函数
friend ostream& operator<<(ostream& out, const Date& d);

同时为了支持实现连续插入cout << d1 << d2 << endl;,所以要有一个返回值且必须返回ostream类型,出了函数作用域没有销毁,所以可以使用引用返回。

C++中支持运算符重载,就是因为要支持自定义类型。

流提取(>>)运算符重载

image-20230605173239114

流提取运算符重载时不能加Date类型不能加const,因为要改变,同时也要使用友元函数。incin的别名,是istream类型的。

//流提取>>运算符重载
inline istream& operator>>(istream& in, Date& d)
{
    in >> d._year >> d._month >> d._day;
    return in;
}

我们可以将流插入运算符重载和流提取运算符重载改为内联函数。

如果一个函数直接定义在类里面,编译器可能直接当成内联函数处理,声明和定义分离的不会被当成内联函数处理。

内联函数在符号表只有函数符号,没有具体的地址,所以链接的时候找不到。我们直接写在.h文件中,内联函数如果在.h文件中只有声明没有定义就要去符号表中找地址进行链接,但是内联函数符号表中没有地址所以不能进行链接。如果在.h文件中声明和定义都存在就不用再去链接。所以内联函数只能在当前.h文件或者.cpp文件中使用。

日期类的具体代码

Date.h头文件(成员函数声明)

//Date.h头文件
#pragma once
#include <iostream>
using namespace std;
//函数声明中可以带缺省参数,函数定义和声明分离,定义中不能带缺省参数
class Date {
    friend ostream& operator<<(ostream& out, const Date& d);
    friend istream& operator>>(istream& in, Date& d);
public:
    //构造函数,使用全缺省的默认构造函数
    Date(int year = 2023, int month = 6, int day = 4);
    //日期 += 天数
    Date& operator+=(int day);
    //日期 + 天数,
    //不改变this指针指向的内容,所以使用const修饰,后面的函数中只要是不改变*this内容都使用const修饰,使其称为const成员函数
    Date operator+(int day) const;
    //日期-=天数
    Date& operator-=(int day);
    //日期-天数
    Date operator-(int day) const;
    //打印日期函数
    void Print() const;
    //前置++
    Date& operator++();
    //后置++,当要实现后置++重载时,给定一个int类型的参数,没有其他实际意义,只是为了构成重载。
    Date operator++(int);
    //前置--
    Date& operator--();
    //后置--
    Date operator--(int);
    //日期类的大小关系比较
    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;
    //日期-日期 
    int operator-(const Date& d) const;
    //因为日期类里面不涉及资源的申请和释放所以拷贝构造函数、
    //赋值运算符重载函数和析构函数使用编译器自动生成的就可以,不用再对其实现。

private:
    int _year;
    int _month;
    int _day;
};
//内联函数只能在当前文件中使用

//流插入<<运算符重载
//流提取>>运算符重载
inline istream& operator>>(istream& in, Date& d)
{
    in >> d._year >> d._month >> d._day;
    return in;
}
inline ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << "/" << d._month << "/" << d._day << endl;
    return out;
}

Date.cpp文件(成员函数功能实现)

//Date.cpp文件
#define _CRT_SECURE_NO_WARNINGS 1
#include "Date.h"
//构造函数,使用全缺省的默认构造函数
// 获取本月天数
//GetMonthDay函数可能被多次调用,最好将其设置为内联函数;
//函数中存储每月天数的数组最好是用static修饰,存储在静态区,避免每次调用该函数都需要重新开辟数组
inline int GetMonthDay(int year,int month)
{
	//使用数组存储每个月的天数
	static int monthday[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	//注意闰年2月天数为29天,闰年:能被4整除且不能被100整除或者能被400整除
	//判断是否是闰年
	if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
	{
		//判断是否是2月
		if (month == 2)
		{
			//如果是闰年2月+1就是29天
			return monthday[month] + 1;
		}
	}
	//不是直接返回当月天数
	return monthday[month];
}
Date::Date(int year, int month, int day)
{
	//先判断年是否合法
	if (year >= 0
		&& month >= 1 && month < 13
		&& day >= 1 && day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "日期非法" << endl;
		exit(-1);//直接退出程序
	}
}

//日期 += 天数
Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		//复用-=
		*this -= -day;
	}
	else
	{
		_day = day + _day;
		//通过while循环判断日期是否合法
		while (_day > GetMonthDay(_year, _month))
		{
			_day -= GetMonthDay(_year, _month);
			//注意先month++再判断
			_month++;
			if (_month > 13)	
			{
				_month = 1;
				_year++;
			}
			
		}
	}
	return *this;
}
//+不改变原本内容
Date Date::operator+(int day) const
{
	Date tmp(*this);//调用拷贝构造
	//const成员函数不能修改this指针指向的内容
	//*this += day;
	
	//复用+=
	tmp += day;
	return tmp;
}
//Date Date::operator+(int day) const
//{
//	Date tmp(*this);
//	if (_day < 0)
//	{
//		//复用-=
//		//tmp -= - day;
//	}
//	else
//	{
//		tmp._day += day;
//		while (tmp._day > GetMonthDay(tmp._year,tmp._month))
//		{
//			tmp._day -= GetMonthDay(tmp._year, tmp._month);
//			//注意先month++再判断
//			tmp._month++;
//			if (tmp._month > 12)
//			{
//				tmp._year++;
//				tmp._month = 1;
//			}
//		}
//	}
//	return tmp;
//}
//Date& Date::operator+=(int day)
//{
//	*this = *this + day;
//	return *this;
//}

//日期-=天数
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		*this += -day;
	}
	else
	{
		_day -= day;
		while (_day <= 0)
		{
			--_month;
			if (_month == 0)
			{
				_year--;
				_month = 12;
			}
			_day += GetMonthDay(_year, _month);
		}
	}
	return *this;
}
//日期-天数
Date Date::operator-(int day) const
{
	Date tmp(*this);
	tmp -= day;
	return tmp;
}
//打印日期函数
void Date::Print() const
{
	cout << _year << "/" << _month << "/" << _day << endl;
}
//前置++
Date& Date::operator++()
{
	//复用+=
	*this += 1;
	return *this;
}

//后置++要给定一个int类型的参数,没有其他实际意义,只是为了构成重载
Date Date::operator++(int)
{
	Date tmp(*this);
	//复用+=
	*this += 1;
	return tmp;
}
//前置--
Date& Date::operator--()
{
	//复用+=
	*this -= 1;
	return *this;
}

//后置--要给定一个int类型的参数,没有其他实际意义,只是为了构成重载
Date Date::operator--(int)
{
	Date tmp(*this);
	//复用+=
	*this -= 1;
	return tmp;
}
//日期类的大小关系比较
bool Date::operator>(const Date& d) const
{
	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) const
{
	return (_year == d._year && _month == d._month && _day == d._day);
}

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 !(*this == d);
}
//日期-日期 
//下面的代码中复用了许多重载的运算符,所以不需要我们考虑底层实现,直接使用就可以。
int Date::operator-(const Date& d) const
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (*this < d) //复用<
	{
		min = *this;
		max = d;
		flag = -1;
	}
	int n = 0;//用于记录加了多少次1
	while (min != max ) //复用!=
	{
		++min;//复用++
		++n;
	}
	return n * flag;
}

test.cpp文件(功能测试)

//test.cpp文件
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
#include "Date.h"
void test1()
{
	//测试+= 和+ 
	Date d1(2023, 6, 5);
	d1.Print();

	d1 += -300;
	d1.Print();
	Date d2(d1 + 60);
	d2.Print();
}
void test2()
{
	//测试-=和-
	Date d1(2023, 6, 5);
	d1.Print();

	d1 -= -300;
	d1.Print();

	Date d2(d1 - 300);
	d2.Print();
}
void test3()
{
	//测试日期 - 日期
	Date d1(2023, 6, 5);
	d1.Print();

	Date d2(d1 + 300);
	d2.Print();

	cout << (d2 - d1) << endl;
}
void test4()
{
	//测试++和--
	Date d1(2023, 6, 5);
	d1.Print();

	++d1;
	d1.Print();
	--d1;
	d1.Print();
	d1++;
	d1.Print();
	d1--;
	d1.Print();
}
void test5()
{
	//测试关系运算符
	Date d1(2023, 6, 5);
	Date d2(2024, 3, 3);
	Date d3(d1);
	cout << (d1 == d2) << endl; //false
	cout << (d1 == d3) << endl; //true
	cout << (d1 != d2) << endl; //true
	cout << (d1 != d3) << endl; //false
	cout << (d1 > d2) << endl;  //false
	cout << (d1 >= d2) << endl; // false
	cout << (d1 <= d2) << endl; // true
	cout << (d1 < d2) << endl; // true

}
void test6()
{
	//测试流提取和流插入
	Date d1;
	cin >> d1;
	cout << d1;
}
int main()
{
	test6();
	return 0;
}
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。