Bootstrap

【C++修炼之路 第二章】日期类的实现

在这里插入图片描述



实现函数:

1、基本函数

  • 构造函数
  • 拷贝构造
  • 析构函数
  • 打印函数

2、运算符重载

  • 小于 小于等于
  • 大于 大于等于
  • 判断相等
  • 赋值运算
  • 加等于 减等于 (加减等于 一个天数)(改变本身)
  • 加减 (加减 一个天数)(不改变本身)
  • 两个日期之间相隔天数
  • 前置++ 和 后置++(重点)
  • 前置-- 和 后置–

3、流插入 << 和 流提取 >>

  • 流插入:cout 输出 <<
  • 流提取:cin 输入 >>



注意:我们日期类各个函数的实现是声明和定义分离:声明写在头文件,定义写在.cpp文件


4、实现总代码:Date.h 、Date.cpp





1、基本函数 与 类的框架

//Date.h
class Date
{
public:
	// 构造函数
	Date(int year, int month, int day);
	// 拷贝构造函数
	Date(const Date& d);

	// 析构函数:用默认的
	// ~Date(){}

	// 打印函数
	void Print() {
		cout << _year << "/" << _month << "/" << _day;
	}

	
private:
	int _year = 1970;
	int _month = 1;
	int _day = 1;
};


//Date.cpp
// 构造函数
Date::Date(int year, int month, int day)
	:_year(2024)
	, _month(1)
	, _day(1)
{
	_year = year;
	_month = month;
	_day = day;
}
// 拷贝构造函数
Date::Date(const Date& d) {
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
// 打印函数
void Date::Print() {
	cout << _year << "/" << _month << "/" << _day;
}



2、运算符重载


我们先实现 小于 和 判断相等 两个重载函数,其他的函数都可以复用这两个函数实现

2.1 小于

// 赋值运算符重载
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) {
			if (_day < d._day) return true;
		}
	}
	return false;
}

2.2 判断相等

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

2.3 大于、大于等于 、小于等于、不等于

(1)大于

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

(2)大于等于

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

(3)小于等于

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

(4)不等于

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

2.4 赋值运算

// 日期类都是没有指向资源的,只有内置类型:使用默认的赋值运算符重载函数也够了,这里我们手动实现
void Date::operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

2.5 加等于 / 减等于 (加减等于 一个天数)(改变本身)


注意:我们这个函数需要考虑天数是 负数的情况

  • d += -10 相当于 d -= 10
  • d -= -10 相当于 d += 10

注意:+= 和 -= 都是改变日期本身,因此直接修改 *this,同时返回该修改后的日期

为什么要返回该日期?
举例:
int t = 10;
t += 1;
本质上 这个计算也需要返回 t,即返回左操作数,相当于更新操作数

// += 和 -= 要考虑 days 是负数的情况
Date& Date::operator+=(int days)
{
	if (days < 0) {
		(*this) -= (-days);
		return *this;
	}
	_day += days;
	while (_day > GetMonthDay(_year, _month)) {
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13) {
			_month = 1;
			_year++;
		}
	}
	return *this;
}

Date& Date::operator-=(int days)
{
	if (days < 0) {
		(*this) += (-days);
		return *this;
	}
	_day -= days;
	while (_day < 0) {
		_month--;
		if (_month ==  0) {
			_month = 12;
			_year--;
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

// 获取该月的天数
// 因为这个函数会频繁的调用,因此写在类里面(其实类里面的函数都是内联函数)
int GetMonthDay(int year, int month)  const {
	assert(month > 0 && month < 13);
	static int  Month[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	// 细节优化技巧
	// 这里将 Month写成静态的非常有意义:因为这个函数会频繁的调用,若 Month 开在栈上,则会频繁的销毁创建
	if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)) return 29;
	else return Month[month];
}

实现思路:这里解释 += 函数的思路,-= 差不多
通过 GetMonthDay函数获取本月的天数
先_day += days;
然后while循环,不断 _day -= GetMonthDay(_year, _month); 一直减到天数无法凑满当月的天数
其中,不断进位,月进位,年进位



2.6 加减 (加减 一个天数)(不改变本身)

你可以发现,大部分函数都是复用前面实现的一小部分函数(这也是开发技巧,不要盲目的作重复的事,要懂得复用已经实现的东西)

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

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

因为 加减是不改变本身的,但是本身会存在执行的加减操作后的数,所有要返回一个加减后的数


int t = 10;
t + 10; // 这个操作不会改变 t
t = t + 10; // 虽然没有改变 t,但是 t + 10 这个表达式是有结果的



2.7 两个日期之间相隔天数

思路:以一个日期为基础,while 循环 +1 或 -1 不断靠近另一日期,最终相等,过程中累计的天数就是 两个日期之间相隔天数
这个方法看起来有些耗时,但实际上现在的计算机一秒可以计算两亿多次,所以影响不会很大(若有更好的方法,欢迎大佬在评论区分享)

int Date::operator-(const Date& d)
{
	int flag = 1, cnt = 0;
	if ((*this) > d) flag = -1;
	Date tmp = (*this);
	while (tmp != d)
	{
		tmp += flag;
		cnt++;
	}

	return flag * cnt;
}



2.8 前置++ 和 后置++(重点)

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

有没有发现这两个函数名相同:没错这里就是 函数重载
函数重载和运算符重载没有任何关系,各论各的
多个 运算符重载函数 可以构成 函数重载:即同名运算符重载函数

为什么 第二个函数有个 int 形参?
仅仅是为了区分,构成函数重载,给后置++,强行增加一个 int 形参(不用写形参名)
int 取什么值都行,这里仅仅只是祖师爷老本为了区分加上去的语法,没有其他的含义

后置++ 设计的思路:
后置++返回+1之前的值,同时本身已经+1
这里也就明白了后置++ 底层的运行逻辑:本身已经+1,只不过需要返回的值是之前的值

同时注意:若需要自定义类型++ 最好是前置++,后置++还要拷贝一次



2.9 前置-- 和 后置–

原理参考:前置++ 和 后置++

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

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



3、流插入 << 和 流提取 >>

为什么要写这个重载函数?
为了使用 cout 和 cin 直接 输入输出 自定义类型

3.1 概念和原理实现

“<<” 是 流插入运算符
“>>” 是 流提取运算符

查看一下C++文档:
在这里插入图片描述
我们可以直接 cout << 10; 打印一个 int 等其他内置类型的数据到控制台上
左边的是第一个操作数 cout 、 中间的是 流插入运算符<< 、 右边的是第二个操作数 int 类型的 10
对应重载函数为 :ostream& operator<<(int val);

我们要想 cout 一个自定义类型
设计的重载函数是:ostream& operator<<(ostream & out);
使用格式为:至于为什么要传一个 ostream & 类型的 cout 参数过去,这里不讨论

// 调用函数的写法
d1.operator<<(cout); 
// 直接运用运算符的写法:<< 
d1 << cout;

你可以发现,根据现有的函数写法,我们只能这样写:d1 << cout;
明显不正常

作为类的重载函数,函数本身会隐藏一个 this指针变量在 函数参数的一号位
即上面的 d1 :一号位参数放在左边
若想把 d1 放在 右边,cout 放在左边(符合常规格式)
就要想办法将 this指针 放置 二号位
但是又因为:函数是成员函数时,this 永远霸占第一个位置
因此,要想达到目的,就不能将该函数写成 成员函数了,需要变成全局函数,可以无限制的,自己调整函数参数的位置
全局函数:ostream& operator<<(ostream& out, Date& d)
就可以这样写了:cout << d1; 可读性极强

问题又来了!为什么连续输出不行 cout << d1 << d2;

想想之前的连续赋值的实现原理:

a = b = c = 10;

原理:每次赋值后,返回左操作数,作为新的赋值式的 右操作数
同理,要想实现连续 cout << d1 << d2; 需要返回 ostream& out 右操作符(因为本身你函数的顺序就调过来了)
因为出了函数作用域,out 还在,可以写成引用
out 是 cout 的别名,cout 是全局对象

3.1 函数实现

// 流插入函数
ostream& operator<<(ostream& out, Date& d) {
	out << d._year << "/" << d._month << "/" << d._day;
	return out;
}

// 流提取函数
istream& operator>>(istream& in, Date& d) {
	in >> d._year >> d._month >> d._day;
	if (!d.IsVaild()) {
		cout << "日期不合法\n";
	}
	return in;
}
bool Date::IsVaild()
{
	if (_year < 0 || _month < 0 || _month > 12 || _day < 0 || _day > GetMonthDay(_year, _month)) return false;
	return true;
}

因为这两个全局函数都需要使用到 对象的私有成员
无法直接访问
则将这两个函数写成友元函数(在本篇博客中有讲解)





4、总代码

Date.h

#pragma once
#include<iostream>
using namespace std;


class Date
{
	friend ostream& operator<<(ostream& out, Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	// 构造函数
	Date(int year, int month, int day);
	// 拷贝构造函数
	Date(const Date& d);

	// 析构函数:用默认的
	// ~Date(){}

	// 重载
	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;

	// 赋值运算符重载
	void operator=(const Date& d);


	// 日期 加减 等于 天数
	// 因为对象定义在 main函数,这里就可以返回 引用
	Date& operator+=(int days);
	Date& operator-=(int days);

	// 日期 加减 天数
	// 因为对象定义在 main函数,这里就可以返回 引用
	Date& operator+(int days);
	Date& operator-(int days);

	// 日期 减 日期
	int operator-(const Date& d);

	// 前置++ 和 后置++
	Date& operator++();
	Date& operator++(int);

	// 获取日期
	int GetMonthDay(int year, int month);

	// 打印函数
	void Print();

	// 判断日期是否合法
	bool IsVaild();
	
private:
	int _year = 1970;
	int _month = 1;
	int _day = 1;
};


Date.cpp

#include "Date.h"

// 构造函数
Date::Date(int year, int month, int day)
	:_year(2024)
	, _month(1)
	, _day(1)
{
	_year = year;
	_month = month;
	_day = day;
}
// 拷贝构造函数
Date::Date(const Date& d) {
	_year = d._year;
	_month = d._month;
	_day = d._day;
}


// 赋值运算符重载
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) {
			if (_day < d._day) return true;
		}
	}
	return false;
}

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

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)._year == d._year &&
		(*this)._month == d._month &&
		(*this)._day == d._day);
}

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

// 日期类都是没有控制资源的 内置类型:使用默认的赋值运算符重载函数也够了
void Date::operator=(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

// += 和 -= 要考虑 days 是负数的情况
Date& Date::operator+=(int days)
{
	if (days < 0) {
		(*this) -= (-days);
		return *this;
	}
	_day += days;
	while (_day > GetMonthDay(_year, _month)) {
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13) {
			_month = 1;
			_year++;
		}
	}
	return *this;
}

Date& Date::operator-=(int days)
{
	if (days < 0) {
		(*this) += (-days);
		return *this;
	}
	_day -= days;
	while (_day < 0) {
		_month--;
		if (_month ==  0) {
			_month = 12;
			_year--;
		}
		_day += GetMonthDay(_year, _month);
	}
	return *this;
}

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

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

int Date::operator-(const Date& d)
{
	int flag = 1, cnt = 0;
	if ((*this) > d) flag = -1;
	Date tmp = (*this);
	while (tmp != d)
	{
		tmp += flag;
		cnt++;
	}

	return flag * cnt;
}

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

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

int Date::GetMonthDay(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)) Month[2] += 1;
	return Month[month];
}

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

bool Date::IsVaild()
{
	if (_year < 0 || _month < 0 || _month > 12 || _day < 0 || _day > GetMonthDay(_year, _month)) return false;
	return true;
}

// 流插入函数
ostream& operator<<(ostream& out, Date& d) {
	out << d._year << "/" << d._month << "/" << d._day;
	return out;
}

// 流提取函数
istream& operator>>(istream& in, Date& d) {
	in >> d._year >> d._month >> d._day;
	if (!d.IsVaild()) {
		cout << "日期不合法\n";
	}
	return in;
}
;