C++类和对象中篇

发布于:2024-05-09 ⋅ 阅读:(45) ⋅ 点赞:(0)

在这里插入图片描述

🐇

🔥博客主页: 云曦
📋系列专栏:[C++]

💨路漫漫其修远兮 吾将而求索
💛 感谢大家👍点赞 😋关注📝评论

📔前言

  • 上期讲解到了C++一个类是如何定义的即对象的实例化等。这期将讲解类的六个默认成员函数及其实现一个日期类。

📔1、类的六个默认成员函数

  • 如果一个类没有任何成员,那么这个类就称为空类。
  • 空类就真的什么都没有嘛?其实不是,任何一个什么都没有的类里,编译器其实默认生成了6个默认成员函数。
  • 默认成员函数:用户没有写,编译器会自动生成的成员函数叫作默认成员函数。
    在这里插入图片描述

📔2、构造函数

📙2.1、概念

对于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;
	Date d2;
	d1.Init(2024, 5, 2);
	d2.Init(2024, 5, 5);

	d1.Print();
	d2.Print();

	return 0;
}
  • 对于日期类来说,可以通过Init来初始化日期,但每创建一个对象都要自己调用一次,这样未免太过于麻烦了,哪有没有在创建对象时就给这个对象初始化好数据的方法呢?
  • 构造函数是一个特殊的函数,函数名与类名相同,创建对象时由编译器自动调用,以此来保证每个对象都有一个合适的初始值,并且在每个对象的生命周期里只能调用一次

📙2.2、特性

  • 构造函数之所以是一个特殊的函数那是因为,构造函数的名字叫构造但它的作用其实并不是开辟空间,而是给调用它的对象进行初始化工作
  • 构造函数的特征:
  1. 函数名与类名相同。
  2. 没有返回值(不用写返回类型)。
  3. 对象在实例化的同时,会自动调用对应的构造函数 。
  4. 构造函数可以重载。
#include<iostream>
using namespace std;

class Date
{
public:
	//无参构造函数
	Date() {};

	//带参的构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

int main()
{
	Date d1;//会去调用无参构造

	Date d2(2024, 5, 2);//会去调用有参构造

	return 0;
}
  • 注意: 在调用无参构造时,对象后面不能带():Date d3();(加了()这样就成了函数声明了)。
  1. 如果类内没有显示写构造函数,那么C++编译器会自动生成一个无参的构造函数,一旦用户显示写构造函数时,编译器就不会再生成了。
#include<iostream>
using namespace std;

class Date
{
public:
	/*Date() {};

	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/

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

int main()
{
	//可以正常运行,因为编译器自动生成了一个无参的构造函数
	Date d1;

	return 0;
}
  1. 关于编译器默认生成的成员函数:大家会有疑惑,用户没实现函数的情况下,编译器会自动生成默认的构造函数,但在我们打印时,打印出来的都是随机值且去调试时也是随机值,就好像编译器默认生成的构造函数什么事都没做一样。
  • 解答:编译器默认生成的构造函数,并不是什么都没做,C++把类型分为了内置类型和自定义类型,编译器默认生成的构造函数对于内置类型不做处理,对自定义类型会去调用它自己的构造函数。
#include<iostream>
using namespace std;

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

private:
	int* _a;
	int _top;
	int _capacity;
};

//两个栈实现队列
class MyQueue
{
public:

private:
	Stack _pushst;
	Stack _popst;
	int size;
};

int main()
{
	MyQueue q;

	return 0;
}

在这里插入图片描述

  • 上述的两个栈实现队列的类就是一个很好的例子。
  • **注意:**在vs2019及之后的vs编译器里,编译器自动生成的构造函数堆内置类型都做了处理,但在vs2013里是没有处理的。这样对于学习C++的伙伴会有误导性,尽管编译器处理了我们还是当作没处理就好。
  • 在C++11中,针对内置类型成员不做初始化的缺陷,又打了补丁,即:内置类型成员变量在声明时,可以给缺省值。
#include<iostream>
using namespace std;

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

private:
	int* _a;
	int _top;
	int _capacity;
};

//两个栈实现队列
class MyQueue
{
public:

private:
	//自定义类型
	Stack _pushst;
	Stack _popst;
	//内置类型
	int size = 1;
};

int main()
{
	MyQueue q;

	return 0;
}

在这里插入图片描述

  1. 无参构造函数和全缺省的构造函数都叫做默认构造函数,并且默认构造函数只能有一个存在(出现一个以上的默认构造函数会有二义性,编译器不知道调用哪个)。
  • **注意:**无参构造、全缺省构造函数、用户不写编译器自动生成的构造函数,都可以认为是默认构造函数。
#include<iostream>
using namespace std;

class Date
{
public:
	//无参构造函数
	Date() {};

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

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

int main()
{
	//对于有一个以上默认构造函数存在的测试
	Date d1;//编译报错:error C2668: “Date::Date”: 对重载函数的调用不明确

	return 0;
}

📔3、析构函数

📙3.1、概念

  • 对于构造函数的学习,我们知道了一个对象是怎么来的,那一个对象又是怎么没的呢?
  • 析构函数:与构造函数的功能相反,析构函数并不是完全给一个对象销毁,局部对象销毁工作是由编译器自动完成的,而对象在销毁时编译器会自动调用析构函数,完成对象的资源清理工作。

📙3.2、特性

  • 析构函数跟构造函数一样也是一个特殊的函数,其特征:
  1. 析构函数名是在类名的前面加上~。
  2. 析构函数没有返回值(不用写返回类型)。
  3. 一个类里只能有一个析构函数,若为显示定义,编译器会默认生成一个析构函数。(注:析构函数不能重载)
  4. 对象的生命周期结束时,编译器会自动调用析构函数。
#include<iostream>
using namespace std;

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

	void Push(int x)
	{
		if (_top == _capacity)
		{
			exit(-1);
		}

		_a[_top] = x;
		++_top;
	}

	//析构函数
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}



private:
	int* _a;
	int _top;
	int _capacity;
};

int main()
{
	Stack st;
	st.Push(1);
	st.Push(2);

	return 0;
}
  • 对象还存在时:
    在这里插入图片描述
  • 对象生命周期结束后:
    在这里插入图片描述
  1. 对于编译器自动生成的析构函数,是否完成过一些事情呢?
#include<iostream>
using namespace std;

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

	void Push(int x)
	{
		if (_top == _capacity)
		{
			exit(-1);
		}

		_a[_top] = x;
		++_top;
	}

	//析构函数
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue
{
public:

private:
	//自定义类型
	Stack _pushst;
	Stack _popst;
	//内置类型
	int size = 1;
};

int main()
{
	MyQueue q;


	return 0;
}

在这里插入图片描述

  • 从上述可以看出,编译器生成的默认析构函数,对于自定义类型会去调用它自己的析构函数。
  1. 如果类中没有资源申请(不会在堆上开空间),我们可以不写析构函数,直接用编译器生成的默认析构函数,比如:Date类;有资源申请(会在堆上开空间)时,一定要写析构函数,否则会造成资源泄漏,比如:Stack类。

📔4、拷贝构造函数

📙4.1、概念

  • 我们在创建对象时,能否创建一个和已存在的对象一模一样的对象呢?
  • 拷贝构造函数:只有单个形参,该对象是对于本类对象的引用(一般常用const修饰),在用已存在的同类型对象去初始化另一个新对象时,由编译器自动调用。

📙4.2、特性

  • 拷贝构造函数也是特殊的成员函数,其特征为:
  1. 拷贝构造函数是构造函数的一个重载形式。
  2. 拷贝构造函数的参数只有一个,且必须是类类型对象的引用,使用传值方式编译器会报错,因为会引发无穷递归。
#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)//错误写法:编译报错,会引发无穷递归
	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;
	}


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

int main()
{
	Date d1(2023, 11, 11);
	Date d2(d1);
	
	d1.Print();
	d2.Print();

	return 0;
}

在这里插入图片描述

  • 拷贝构造函数传值传参的情况:
    在这里插入图片描述
  1. 若用户未显示写拷贝构造函数,编译器会生成一个拷贝构造函数,但编译器生成的拷贝构造函数里是按字节序完成拷贝的,这种拷贝叫作浅拷贝,或值拷贝。
//Date d1(d2);
//d1为:this;d2为d
Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
  • 浅拷贝对自定义类型带来的问题:
#include<iostream>
using namespace std;

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue
{
public:

private:
	Stack _pushst;
	Stack _popst;
	int size = 1;
};

int main()
{
	MyQueue q1;
	MyQueue q2(q1);
	
	return 0;
}

在这里插入图片描述

  • 上述代码在运行时,程序崩溃了。至于为什么,接下来为大家讲解:
    在这里插入图片描述
  • 解决方法就是把拷贝构造函数写成深拷贝。
#include<iostream>
using namespace std;

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

	//深拷贝
	Stack(const Stack& d)
	{
		_a = (int*)malloc(sizeof(int) * d._capacity);
		if (nullptr == _a)
		{
			perror("malloc fail");
			exit(-1);
		}

		_capacity = d._capacity;
		_top = d._top;
	}

	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue
{
public:

private:
	Stack _pushst;
	Stack _popst;
	int size = 1;
};

int main()
{
	MyQueue q1;
	MyQueue q2(q1);

	return 0;
}
  • 注意: 用户没写拷贝构造函数时,编译器会默认生成一个拷贝构造函数,这个拷贝构造函数内置类型成员会进行值拷贝,自定义类型成员会调用它自己的拷贝构造函数。
  • 关于深浅拷贝的问题,C++内存管理篇会详细讲解。
  1. 编译器默认生成的拷贝构造函数是值拷贝,那么我们还要显示实现吗?
  • 解答:像Date这样,类里没有申请资源的类,就没必要写了。
  • **注意:**一旦涉及申请资源的类,一定要写拷贝构造函数且实现的是深拷贝,否则就是浅拷贝。
#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;
};

int main()
{
	Date d1(2024, 5, 2);
	Date d2(d1);

	d1.Print();
	d2.Print();

	return 0;
}

在这里插入图片描述

  1. 拷贝构造函数的典型使用场景:
  • 使用已存在的对象去初始化一个新对象。
  • 函数参数类型为类类型对象(传值传参)
  • 函数返回值类型为类类型(传值返回)
#include<iostream>
using namespace std;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		cout << "Date(int year = 1, int month = 1, int day = 1)" << endl;
	}

	Date(const Date& d)
	{
		cout << "Date(const Date& d)" << endl;
	}

	~Date()
	{
		cout << "~Date()" << endl;
	}


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

Date TestDate(Date dd)
{
	Date tmp(dd);
	return tmp;
}

int main()
{
	Date d1(2023, 12, 12);
	Date d2(d1);
	cout << endl;

	TestDate(d1);

	return 0;
}

在这里插入图片描述

  • 为了提高程序的效率,一般对象传参时,尽量使用引用类型,返回时看实际场景,能用引用就尽量用引用。

📔5、运算符重载

📙5.1、运算符重载

  • 在C++中,为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。
  • 函数名字为:关键字operator后面紧跟需要重载的运算符符号
  • 函数原型:返回类型 operator操作符(参数列表)。
  • 注意:
  • 不能连接其他的符号来创建新的运算符重载,比如:operator@、operator#。
  • 重载操作符必须有一个类类型参数。
  • 用于内置类型的运算符其含义不能改变,比如实现+运算符重载,内部却实现的是减法。
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数是一个隐含的this指针。
  • ( .*、::、sizeof、?:、. ) 注意以上5个运算符不能重载。
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

//在类外实现==运算符重载
//在类外实现有个问题,因为类的成员变量是私有的无法访问到
//这里得让类的成员函数写成公有的,但这时候问题又有了,
//类的成员变量成为公有后,封装的意义何在
bool operator==(Date& d1, Date& d2)
{
	return d1._year == d2._year
		&& d1._month == d2._month
		&& d1._day == d2._day;
}
  • 既然在类外无法访问到类的成员变量,那么我们就定义到类里面,让运算符重载变成类的成员函数,这样就可以访问到类的成员变量了。
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	//需要注意的是:左操作数是this,右操作数是指向调用的对象
	//bool operator==(Date* this, Date& d)
	bool operator==(Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

private:
	int _year;
	int _month;
	int _day;
};
  • 对于运算符的重载大家些许会有疑问,既然有运算符重载那么每个类都要把能实现的运算符重载都实现吗?
  • 解答:其实并不是,对于一个类来说:实现什么的运算符重载,是看这个运算符重载对于这个类有没有实现的意义。比如:日期类里实现日期+日期有用途吗?,日期+日期没啥用嘛就不需要实现了。
  • 但如果是:日期加天数呢?唉!日期加天数不就是看多少天后的日期嘛,嗯…,这个运算符重载对日期类是有意义的,那么就实现它。
  • 思路:
    在这里插入图片描述
#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;
	}

	//需要注意的是:左操作数是this,右操作数是指向调用的对象
	//bool operator==(Date* this, Date& d)
	bool operator==(Date& d)
	{
		return _year == d._year
			&& _month == d._month
			&& _day == d._day;
	}

	//因为12个月的天数除了二月其他都是固定的
	//所以写一个获取本年本月内天数的函数
	int GetMonthDaye(int year, int month)
	{
		int Month[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		//判断本年的2月是否是闰年还是平年
		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
		{
			return 29;
		}

		return Month[month];
	}

	//这里传值返回会生成临时对象返回,等于会调用一次拷贝构造函数
	//用引用返回就不一样,因为是返回this,
	//this出了函数还在引用返回没有问题且引用返回不用调用拷贝构造
	Date& operator+(int day)
	{
		_day += day;

		//循环结束条件:_day小于或等于当月的天数时,循环结束
		while (_day > GetMonthDaye(_year, _month))
		{
			_day -= GetMonthDaye(_year, _month);
			++_month;
			if (_month == 13)//判断月份是否大于12月
			{
				++_year;
				_month = 1;
			}

		}

		//this是d1对象的指针,*this就是d1
		return *this;
	}

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

int main()
{
	Date d1(2024, 5, 3);
	d1 + 50;
	d1.Print();

	return 0;
}

在这里插入图片描述

  • 上述的代码,仔细的朋友们可能已经发现问题了,这里实现的是什么,是+的运算符重载,+在内置类型里是不会改变变量本身的,而日期类实现的+运算符重载是把d1本身给修改了。显而易见这不是+运算符的重载,而是+=运算符的重载。
#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;
	}

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

	int GetMonthDaye(int year, int month)
	{
		int Month[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 Month[month];
	}

	Date& operator+=(int day)
	{
		_day += day;

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

		}

		return *this;
	}

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

int main()
{
	Date d1(2024, 5, 3);
	d1 += 50;
	d1.Print();

	return 0;
}
  • 嗯…这样就好多了。
  • 那+运算符重载要咋样实现呢?这里有的人就会说,把上面的代码复制粘贴一下,然后定义一个临时对象修改这个临时对象即可,this指向的对象就不用修改,最后返回这个临时对象。
Date operator+(int day)
	{
		Date tmp(*this);
		tmp._day += day;

		while (tmp._day > GetMonthDaye(tmp._year, tmp._month))
		{
			tmp._day -= GetMonthDaye(tmp._year, tmp._month);
			++tmp._month;
			if (tmp._month == 13)
			{
				++tmp._year;
				tmp._month = 1;
			}

		}

		return tmp;
	}

在这里插入图片描述

  • 这样确实可以,但。。。是不是过于繁琐了,我们不是已经实现了+=运算符重载了吗,直接让+复用+=就好了。
Date operator+(int day)
	{
		Date tmp(*this);
		tmp += day;
		return tmp;
	}

📙5.2、赋值运算符重载

  • 下面的代码是拷贝构造还是赋值拷贝?
void TestDate1()
{
	Date d1(2024, 12, 12);
	Date d2 = d1;
}
  • 答案是:拷贝构造。
  • 讲解:
void TestDate1()
{
	Date d1(2024, 12, 12);
	//一个已存在的对象去拷贝初始化另一个对象,拷贝构造
	Date d2 = d1;

	//两个已存在的对象,赋值拷贝
	d1 = d2;
}
  1. 赋值运算符重载的格式:
  • 参数类型:const type&,传递引用提供程序的效率。
  • 返回类型:type&,因为返回的是this指针,出了函数还存在所以可以用引用返回。
  • 检测是否是自己给自己赋值。
  • 返回*this,复合连续赋值的含义。
  • 加const是避免一下的情况出现:
//*this为d2,d为d1
//原本是d2 = d1,加const是为了避免写成d1 = d2
Date& operator=(const Date& d)
	{
		d._year = _year;
		d._month = _month;
		d._day = _day;
	}
  • 赋值运算符重载
bool operator!=(Date& d)
	{
		//复用==运算符重载
		return !(*this == d);
	}

	Date& operator=(const Date& d)
	{
		//比较地址是否不相同
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}

		return *this;
	}
  1. 赋值运算符只能重载成类的成员函数 ,不能重载成全局函数
//1.赋值运算符重载成全局函数,因为没有this指针需要给两个参数
//2.全局函数访问不到类内的成员变量,因为是私有的
//3.会编译报错:error C2801: “operator =”必须是非静态成员
//原因:赋值运算符重载是六个默认成员函数中的一个,用户不实现编译器会默认自动生成一个
//而编译器默认生成后的赋值运算符与我们实现的全局赋值运算符重载冲突了,
//所以赋值运算符重载只能是类的成员函数
Date& operator=(Date& d1, Date& d2)
{
	if (&d1 != &d2)
	{
		d1._year = d2._year;
		d1._month = d2._month;
		d1._day = d2._day;
	}

	return d1;
}
  1. 用户不实现赋值运算符重载,编译器会生成一个默认的赋值运算符重载,以值的方式逐字节拷贝过去。(注:编译器生成的默认赋值运算符,内置类型会进行值拷贝,对于自定义类型会去调用对于类的赋值运算符重载)。
class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == _a)
		{
			perror("malloc fail");
			exit(-1);
		}

		_top = 0;
		_capacity = capacity;
	}

	void Push(int x)
	{
		if (_top == _capacity)
		{
			exit(-1);
		}

		_a[_top] = x;
		++_top;
	}

	Stack& operator=(const Stack& st)
	{
		cout << "Stack & operator=(const Stack & st)" << endl;
		return *this;
	}

	//析构函数
	~Stack()
	{
		cout << "~Stack()" << endl;
		free(_a);
		_a = nullptr;
		_top = 0;
		_capacity = 0;
	}

private:
	int* _a;
	int _top;
	int _capacity;
};

class MyQueue
{
public:
	MyQueue(int size = 1)
	{
		_size = size;
	}



private:
	//自定义类型
	Stack _pushst;
	Stack _popst;
	//内置类型
	int _size;
};


int main()
{
	MyQueue q1(10);
	MyQueue q2(20);
	q1 = q2;

	return 0;
}

在这里插入图片描述

  • 注意:如果实现的类里没有申请空间的成员函数,那么就不需要去实现赋值运算符重载里,用编译器默认生成的赋值运算符重载即可。

📙5.3、前置++和后置++重载

  • ++,嗯…简单直接去复用+=即可。
//前置++
	Date& operator++()
	{
		*this += 1;
		return *this;
	}
  • 但主要的问题是:后置++怎么进行重载呢?
  • 有的伙伴也许会直接想,直接把++放在operator前面就行Date& ++operator(),很可惜,编译错误,没有这样的语法。那要怎样才能重载前置++呢?
  • 其实是这样的,C++为了解决后置++这个问题,弄出了占位符这个语法:
  • 在函数的形参用一个类型来进行重载,一般使用int来当占位符,但其他类型也是可以的(只是正常都用int来当) 。Date operator++(int),这个函数的参数在实参传形参时可以传也可以不传。
//后置++
	Date operator++(int)
	{
		Date tmp = *this;
		*this += 1;
		return tmp;
	}
  • 注意:
  1. C++已经强制了,无参的重载为前置++,有参的重载为后置++。
  2. 内置类型的前置和后置++区别不大,但自定义类型进行++时,要考虑好再选择用前置还是后置,因为后置++的返回值必须是值返回,值返回会造成一次拷贝构造。

📔6、日期类的实现

  • 既然是实现日期这个类,那么先把日期类进行一下分文件实现。
  • Date.h(放声明)
#pragma once
#include<iostream>
using std::cout;
using std::cin;
using std::endl;

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1);
	Date(const Date& d);

	bool operator==(Date& d);
	bool operator!=(Date& d);

	int GetMonthDaye(int year, int month);
	void Print();

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


	Date& operator=(const Date& d);

	Date& operator++();
	Date operator++(int);

private:
	int _year;
	int _month;
	int _day;
};
  • Date.cpp(放函数的定义)
#include"Date.h"

Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

Date::Date(const Date& d)
{
	cout << "Date(const Date& d)" << endl;
}

void Date::Print()
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

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

int Date::GetMonthDaye(int year, int month)
{
	int Month[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 Month[month];
}

Date& Date::operator+=(int day)
{
	_day += day;

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

	}

	return *this;
}

Date Date::operator+(int day)
{
	Date tmp(*this);
	tmp += day;
	return tmp;
}

bool Date::operator!=(Date& d)
{
	//复用==运算符重载
	return !(*this == d);
}

Date& Date::operator=(const Date& d)
{
	//比较地址是否不相同
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	return *this;
}

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

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

📙6.1、日期类比较运算符重载

  • >、<、!=、==、>=、<=,这些比较运算符对日期类都是有用的。

📄6.1.1、等于运算符重载

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

📄6.1.2、不等于运算符重载

bool Date::operator!=(Date& d)
{
	//复用==运算符重载
	return !(*this == d);
}

📄6.1.3、大于运算符重载

//把正确的都判断出来,剩下的都表示d1不大于d2
bool Date::operator>(Date& d)
{
	//年大于就返回true
	if (_year > d._year)
	{
		return true;
	}
	//年相等且月大于月就返回true
	else if (_year == _year && _month > d._month)
	{
		return true;
	}
	//年相等且月相等且天大于天就返回true
	else if (_year == _year && _month == d._month && _day > d._day)
	{
		return true;
	}

	return false;
}

📄6.1.4、小于运算符重载

bool Date::operator<(Date& d)
{
	//复用==和>运算符重载
	return !(*this == d || *this > d);
}

📄6.1.5、大于等于运算符重载

bool Date::operator>=(Date& d)
{
	//复用<运算符重载
	return !(*this < d);
}

📄6.1.6、小于等于运算符重载

bool Date::operator<=(Date& d)
{
	//复用>运算符重载
	return !(*this > d);
}

📙6.2、日期类加天数的运算符重载

  • 一个日期+=或+天数,可以知道多少天后日期。
  • 一个日期-=或-天数,可以知道多少天前的日期。

📄6.2.1、日期+=天数

Date& Date::operator+=(int day)
{
	_day += day;

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

	}

	return *this;
}

📄6.2.2、日期+天数

Date Date::operator+(int day)
{
	Date tmp(*this);
	tmp += day;
	return tmp;
}

📄6.2.3、日期-=天数

在这里插入图片描述

Date& Date::operator-=(int day)
{
	_day -= day;
	while (_day <= 0)
	{
		--_month;
		if (_month == 0)
		{
			--_year;
			_month = 12;
		}

		_day += GetMonthDaye(_year, _month);
	}

	return *this;
}

在这里插入图片描述

📄6.2.4、日期-天数

Date Date::operator-(int day)
{
	//和+运算符重载一样
	//复用-=运算符重载
	Date tmp(*this);
	tmp -= day;
	return tmp;
}

在这里插入图片描述

📄6.2.5、GetMonthDay函数

int Date::GetMonthDaye(int year, int month)
{
	int Month[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	//小细节:把2月放到前面判断,可以减少其他月的判断
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0))
	{
		return 29;
	}

	return Month[month];
}

📙6.3、日期-日期运算符重载

  • 这个运算符重载还是有用的,日期减日期得到相差的天数。
int Date::operator-(Date& d)
{
	//左大右小相差的天数是正数
	int flag = 1;
	//d1 - d2
	//假设左大右小
	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;
}

📙6.4、前置和后置++/- -运算符重载

📄6.4.1、前置++

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

📄6.4.1、后置++

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

📄6.4.1、前置- -

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

📄6.4.1、后置- -

Date Date::operator--(int)
{
	Date tmp(*this);
	*this -= 1;
	return tmp;
}

📙6.5、流插入/流提取函数重载

  • 在这个日期类的实现到现在,始终是在用实现的Print成员函数来输出日期,那么为什么不用cout打印呢。
  • 其实<<、>>,流插入和流提取也是可以进行运算符重载的,当我们要用流插入输出一个类时,编译器不知道你要怎么输出这个类,所以用流插入输出时需要先实现这个运算符的重载。
  • 注意:
  1. <<运算符重载的返回值是流插入,即:ostream
  2. >>运算符重载的返回值是流提取,即:istream

📙6.5.1、<<运算符重载

  • 类内实现
//引用返回,支持<<在一行内可以连续插入
ostream& Date::operator<<(ostream& out)
{
	out << _year << "-" << _month << "-" << _day << endl;

	return out;
}
  • 实现完后会发现问题:
void TestDate4()
{
	Date d1(2024, 12, 12);
	cout << d1 << endl;
	//编译报错:error C2679: 二元“<<”: 
	//没有找到接受“Date”类型的右操作数的运算符(或没有可接受的转换)
}
  • 编译报错,怎么回事!其实类内实现有个特点,语法上的传参:cout << d1 << endl; -> cout << d1.operator(cout) << endl; -> cout << d1.operator(&d1,cout) << endl;
  • 类的成员函数传参时,对象一定是占用第一个参数位置的(this指针永远是第一个参数)。
  • 类内实现的输入输出运算符重载,得这样调用d1<<cout<<endl,可这样就不易读懂,所以输出输入运算符重载,只能在类外实现,让插入或提取流做第一个参数。
  • 类外实现
ostream& operator<<(ostream& out, Date& d)
{
	out << d._year << "-" << d._month << "-" << d._day << endl;

	return out;
}
  • 这样又出现问题了,类的成员变量是私有的无法访问到,写成公有的就不符合C++封装的概念了。
  • 解决方法:
  1. 写提供成员变量值的函数出来,例如:GetYear、GetMonst、GetDay。
  2. 友元(关键字:friend),举例:我的是我的,你的也是我的,但我的不是你的(这里只是让大家了解一下友元,具体内容会到类和对象下讲解)。
  • 这里用友元来解决这里的问题。
    在这里插入图片描述

6.5.2、>>运算符重载

  • 提取流运算符重载也跟插入流运算符一样,必须在类外实现成全局函数,通过友元方式访问类的成员变量。
istream& operator>>(istream& in, Date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

📙6.6、解决日期类的一些bug

📄6.6.1、构造函数

Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
  • 这里实现的构造函数有一个问题,年月日都没有小于等于0和月大于12,天大于当月的情况,所以构造函数这里要检查一下初始化的日期是否正确。
  • 不合法的日期我们可以采集一下任意一个措施:
  1. assert(false); 直接强制报错
  2. exit(-1); 直接终止程序
  3. 先打印一下日期,然后打印非法日期。
  • 这里我采用第三中措施。
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;

	if (_year <= 0 || _month <= 0 
		|| _month > 12 || _day <= 0 
		|| _day > GetMonthDaye(year, month))
	{
		Print();
		cout << "非法日期" << endl;
	}
	
}

📄6.6.2、日期-=、-、+=、+天数

  • 一个日期 + 天数可以得到这个天数后的日期,如果加的是负数,那就成这个天数前的日期了,提前处理一下比较好。
  • -=运算符重载处理
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += (-day);
	}

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

		_day += GetMonthDaye(_year, _month);
	}

	return *this;
}
  • -运算符重载处理
Date Date::operator-(int day)
{
	Date tmp(*this);
	if (day < 0)
	{
		return tmp += (-day);
	}

	tmp -= day;
	return tmp;
}
  • +=运算符重载处理
Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= (-day);
	}

	_day += day;

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

	}

	return *this;
}
  • +运算符重载处理
Date Date::operator+(int day)
{
	Date tmp(*this);
	if (day < 0)
	{
		return tmp -= (-day);
	}

	tmp += day;
	return tmp;
}

📔7、const成员函数

  • const修饰的“成员函数”叫作const成员函数,const修饰的成员函数实际上修饰的是成员函数隐含的this指针,表示在该成员函数内不能修改任何成员变量。
  • 注意:
  1. 非const对象也可以调用const成员函数。
  2. const成员函数是只读状态,建议大家实现成员函数时,如果成员变量不做修改那么就加上const,增加程序的安全性。
  3. 像流插入、流提取这样在类外重载的函数,就不用加const了,因为类外实现的函数没有隐含的this指针。
  • 在函数声明后面加const成为const成员函数。例如:日期类的比较运算符重载,不修改成员变量那么是可以加const的。
class Date
{
	friend ostream& operator<<(ostream& out, Date& d);
	friend istream& operator>>(istream& in, Date& d);

public:
	Date(int year = 1, int month = 1, int day = 1);

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

	int GetMonthDaye(int year, int month) const;
	void Print() const;

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

	Date& operator++();
	Date operator++(int);
	Date& operator--();
	Date operator--(int);

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

ostream& operator<<(ostream& out, Date& d);
istream& operator>>(istream& in, Date& d);
  • 在类里加const时需要注意以下几个问题:
  1. const对象可以调用非const成员函数吗? – 不可以(权限的放大)
  2. 非const对象可以调用const成员函数吗?-- 可以(权限的缩小)
  3. const成员函数内可以调用其它的非const成员函数吗? – 不可以(权限的放大)
  4. 非const成员函数内可以调用其它的const成员函数吗? – 可以(权限的缩小)

📔8、取地址及const取地址运算符重载

	Date* operator&();
	const Date* operator&() const;
  • 这两个默认成员函数,一般不用实现,因为编译器会默认生成。
Date* Date::operator&()
{
	return this;
}

const Date* Date::operator&() const
{
	return this;
}
  • 这两个函数不需要重载,使用编译器生成的默认取地址重载即可,除非是特殊情况,才需要重载,比如:想让别人获取特定的内容!