手把手教你写一个日期计算器(C++)

放弃信念,无异死亡。


我们规范一点,还是采用分文件的方式来写。

💖1.声明日期类

我们在头文件里声明我们的日期类。对于日期类而言,只要写一个构造函数就够了。因为日期类里全是内置类型,默认的构造函数不对其处理,而默认生成的拷贝构造函数和赋值运算符重载函数已经够用了(值拷贝),也不需要析构函数。在这里运算符重载对我们来说才是真正的重点。
ok,我们先简单开个头:因为要对日期类进行运算,在声明日期类的时候,顺便写一个能够获取某年某月的天数的函数,方便后续进行运算。

Date.h

#pragma once
#include <iostream>
using std::cout;
using std::cin;
using std::endl;


class Date
{

public:
	//判断是否为闰年
	bool isLeapYear(int year)
	{
		return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
	}

	//获取天数
	int GetMonthDay(int year, int month);

	//构造函数
	Date(int year = 1, int month = 1, int day = 1);

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

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

🌟2.构造函数和获取天数

🔥2.1获取天数

int Date::GetMonthDay(int year, int month)
{
	assert(year >= 0 && month > 0 && month < 13);
	//加static是为防止函数频繁调用每次都开辟数组空间。
	const static int monthDayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	if (month == 2 && isLeapYear(year))
	{
		return 29;
	}
	else
	{
		return monthDayArray[month];
	}
}

☀️2.2 构造函数

Date::Date(int year, int month, int day)
{
	if (year >= 1 &&
		month <= 12 && month >= 1 &&
		day >= 1 && day <= GetMonthDay(year, month))
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "非法访问" << endl;
	}
}

⚡️*3.关系运算符重载

关于运算符重载我们不用全部都实现,对于逻辑上有关联的一对运算符,我们只要实现其中一个就可以了,另一个可以通过逻辑运算符复用写好的那一个。

🌊3.1 < 和 ==

假设我们实现了小于和等于的运算符重载,那我们就可以利用这两个函数去实现其它运算符。这种方式不仅仅适用于日期类,其它的自定义类型需要写运算符重载函数的,都可以按照这种思想去写。

//小于运算符重载
bool Date::operator<(const Date& d)
{
	if ((_year < d._year)
		|| (_year == d._year && _month < d._month)
		|| (_year == d._year && _month == d._month && d._day < d._day))
	{
		return true;
	}
	else
	{
		return false;
	}
}

//等于运算符重载
// d1 == d2
bool Date::operator==(const Date& d)
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}

🐮3.2 <=

*this 就是d1,d是d2的别名。这里复用了<和=,有了这个例子,其它的对于大家来说是不是就很简单了。

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

🐸3.3 >

大于就是<=取反。

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

🌺3.4 !=

!=就是==取反。

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

🍀3.5 测试

void TestDate1()
{
	Date d1(2022, 5, 18);
	Date d2(2023, 3, 20);
	Date d3(2023, 3, 20);

	cout << (d1 < d2) << endl;
	cout << (d1 > d2) << endl;
	cout << (d1 == d3) << endl;
	cout << (d2 <= d3) << endl;
	cout << (d2 == d3) << endl;
}

注意:流插入运算符运算级非常的高,我们为了然日期类对象先进行运算必须加()

运行结果
在这里插入图片描述

🌾4. inline优化

我们发现关系运算符重载的代码都挺短的,我们可以把它们写成内联函数进行优化。但是内联函数的声明和定义不能分离, 因为内联函数不会把函数名放进Data.o的符号表,声明在Date.cpp展开后链接不上。
这个时候我们把复用了其他运算符的重载函数直接在类里面定义,连inline都不用加了,因为类里面默认是内联函数。

其实我们都可以把它们放进类里,因为日期类的运算符重载都不太长,都放过去也不太好。而且为了演示分文件的过程,我们把需要自己实现的,在类外面定义。复用的放在类里面。

#pragma once
#include <iostream>
using std::cout;
using std::cin;
using std::endl;


class Date
{
public:
	//判断是否为闰年
	bool isLeapYear(int year)
	{
		return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
	}

	//获取天数
	int GetMonthDay(int year, int month);

	//构造函数
	Date(int year = 1, int month = 1, int day = 1);

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

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

	// inline不支持声明和定义分别放到.h 和.cpp
	// 所以成员函数中要成为inline最好直接在类里面定义
	// 类里面定义默认就是inline
	bool operator>(const Date& d)  const
	{
		return !(*this <= d);
	}

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

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

	// d1 <= d2
	bool operator<=(const Date& d)  const
	{
		return *this < d || *this == d;
	}
private:
	int _year;
	int _month;
	int _day;
};

🌼*5 算术运算符重载

🌙5.1 +

加法运算进位要考虑当月的天数,月份是否合法,此时我们写的获取天数的函数就可以用上了。边界控制好了,就好说了。
在这里插入图片描述

Date Date::operator+(int day)
{
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}

	return *this;
}

大家想一想这个代码对不对?管他对不对,我们来测试一下:

// d1 + 100 -->d1.operator+(day);
void TestDate2()
{
	Date d1(2022, 5, 18);
	Date d2 = d1 + 15;
	d2.Print();
	d1.Print();
}

运行结果:
在这里插入图片描述
因为d1的地址传给了隐含的this指针,_year、_month、_day其实是this->_year、this->month、this->day。上面写的代码一上来就把自己改了,这不是我们所期望的。像i+100是不会变的,i+=100才会变。这个问题很容易解决,我们先拷贝一个。

正确代码:

Date Date::operator+(int day)
{
	//调用拷贝构造函数
	Date ret(*this);

	ret._day += day;
	while (ret._day > GetMonthDay(ret._year, ret._month))
	{
		ret._day -= GetMonthDay(ret._year, ret._month);
		ret._month++;
		if (ret._month == 13)
		{
			++ret._year;
			ret._month = 1;
		}
	}

	return ret;
}

运行结果:
在这里插入图片描述
注意:ret出了函数作用域就销毁了,不能用引用做返回值,不然就会出现野指针问题。 可能你用引用做返回值有时候结果还是正确的,这个时候只不过是编译器没有清理栈帧而已,你再调用其他函数试试。

🌏5.2 +=

我们之前写的+的错误代码是不是就是+=的逻辑呀!+=是有返回值的,因为要支持连续的+=。
比如说:d2 += d1 += 100

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

	return *this;
}

因为*this就是d1,出了作用域不会销毁,this才会销毁,所以可以用引用返回,减少一次传值返回的拷贝。

测试:

void TestDate3()
{
    Date d1(2022, 5, 18);
	Date d2 = d1 + 15;
	Date d3;
	d3 = d1 + 15;
	d2.Print();
	d1.Print();
	d1 += 15;
	d1.Print();
}

运行结果:
在这里插入图片描述

🎃5.3 +和+=谁复用谁比较好?

+复用+=效率高,这样节省了两次拷贝。
在这里插入图片描述
同样算术运算符的重载还是把复用的写进类里,自己实现的在类外定义。

🎉5.4 -=

我们先写-=,等会-复用-=。减需要借位,借位借的是前一月的,且0月不能借位。注意控制好边界。
注意:如果减的是负数,我们就加上它的相反数。
在这里插入图片描述

Date& Date::operator-=(int day)
{
	if (day < 0)
		return *this += -day;

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

		_day += GetMonthDay(_year, _month);
	}

	return *this;
}

测试:

void TestDate3()
{
	Date d3(2022, 5, 18);
	d3 -= -100;
	d3.Print();
	d3 += 10000;
	d3.Print();
	d3 -= 10000;
	d3.Print();
}

运行结果:
在这里插入图片描述
在这里插入图片描述

🎁5.5 - (Date-整数)

Date Date::operator-(int day)
{
	Date ret = *this;
	ret -= day;
	return ret;
}

🎅5.6 前置++和后置++

前置++和后置++运算符重载都是Date operator++();如何区分呢?
C++在语法上给了一些特殊处理:要想区分,必须构成函数重载,函数名相同,参数必须不同。

Date operator++();
//只是为了区分,参数不需要具体的值
Date operator++(int);

C++语法规定不带参数的是前置,带参数的是后置。

实现起来很简单,我们直接调+=

// 前置++
// ++d -> d.operator++(&d)
Date& operator++()
{
	*this += 1;
	return *this;
}

后置++要返回++之前的值,可以先拷贝一个,但是ret出了作用域就销毁了,不能用引用返回。

// 后置++
// d++ -> d.operator++(&d, 0)
Date operator++(int)
{
	Date ret(*this);
	*this += 1;
	return ret;
}

测试及运行结果:
在这里插入图片描述

🎄5.7 日期-日期 返回天数

首先,日期-日期和日期-天数是构造函数重载的。日期-天数的参数是int,日期-日期的参数是日期类。
博主教大家用一种最简单的方法来实现。
我们不知道d1、d2谁小谁大,我们可以先默认d1大,d2小,后面再修正。flag来代替±号。

// d1 - d2
int Date::operator-(const Date& d)
{
	int flag = 1;
	Date max = *this;
	Date min = d;
	//修正
	if (*this < d)
	{
		min = *this;
		max = d;
		flag = -1;
	}

	int n = 0;
	//复用!=和前置++
	//max和min相差几天n就+几天
	while (min != max)
	{
		++n;
		++min;
	}

	return n * flag;
}

测试及运行结果:
在这里插入图片描述
在这里插入图片描述

🔮6 关于拷贝构造函数的一些思考

📷6.1 Date d2 = d1

void TestDate2()
{
	Date d1(2022, 5, 18);
	Date d2 = d1;
}

大家觉得Date d2= d1;是拷贝构造还是赋值?
在这里插入图片描述
其实它也会去调拷贝构造函数,而不是赋值运算符重载。

结论:

int main()
{
	Date d1(2022, 5, 16);
	Date d2(2022, 5, 18);

	Date d3(d1); // 拷贝构造  -- 一个存在的对象去初始化另一个要创建的对象
	Date d3 = d1;// 拷贝构造  -- 一个存在的对象去初始化另一个要创建的对象
	d2 = d1;     // 赋值重载/复制拷贝 -- 两个已经存在对象之间赋值
	return 0;
}

💻6.2 传值返回的值做引用参数不加const报错

d1+15这个表达式的结果是:传值返回其实返回的是拷贝了ret的临时变量,临时变量具有常性(只能读不能写),再去调用拷贝构造或赋值运算符重载时,权限被放大了,当然会报错。
在这里插入图片描述

📗7 完整代码

📘7.1 Date.h

#include <iostream>
#include <assert.h>
using std::cin;
using std::cout;
using std::endl;

class Date
{
	// 友元函数
	friend std::ostream& operator<<(std::ostream& out, const Date& d);
	friend std::istream& operator>>(std::istream& out, Date& d);
public:
	// 获取某年某月的天数
	int GetMonthDay(int year, int month);

	// 全缺省的构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		if (year >= 1 &&
			month <= 12 && month >= 1 &&
			day >= 1 && day <= GetMonthDay(year, month))
		{
			_year = year;
			_month = month;
			_day = day;
		}
		else
		{
			cout << "非法访问" << endl;
		}
	}

	// 拷贝构造函数
	// d2(d1)
	Date(const Date& d)
	{
		this->_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	// 赋值运算符重载
	// d2 = d3 -> d2.operator=(&d2, d3)
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			this->_year = d._year;
			this->_month = d._month;
			this->_day = d._day;
		}
		return *this;
	}

	bool operator<(const Date& d);

	bool operator==(const Date& d);

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

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

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

	Date& operator-=(int day);

	Date& operator+=(int day);

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

	Date operator-(int day)
	{
		Date ret = *this;
		ret -= day;
		return ret;
	}

	int operator-(const Date& d);

	// 前置++
    // ++d -> d.operator++(&d)
	Date& operator++()
	{
		*this += 1;
		return *this;
	}

	// 后置++
    // d++ -> d.operator++(&d, 0)
	Date operator++(int)
	{
		Date ret(*this);
		*this += 1;
		return ret;
	}

	// // 后置--
	Date operator--(int)

	{
		Date ret(*this);
		*this -= 1;
		return ret;
	}

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

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

📓7.2 Date.cpp

#include "Date.h"

std::ostream& operator<<(std::ostream& out, const Date& d)
{
	out <<d. _year << "-" << d._month << "-" << d._day << endl;
	return out;
}

std::ostream& operator>>(std::ostream& int, Date& d)
{
	in >>d. _year >> "-" >> d._month >> "-" >> d._day >> endl;
	return in;
}

bool Date::operator<( const Date& d)
{
	if ((_year < d._year)
		|| (_year == d._year && _month < d._month)
		|| (_year == d._year && _month == d._month && d._day < d._day))
	{
		return true;
	}
	else
	{
		return false;
	}
}

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

bool isLeapYear(int year)
{
	if ((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0))
	{
		return true;
	}
	return false;
}

int Date::GetMonthDay(int year, int month)
{
	assert(year >= 0 && month > 0 && month < 13);

	const static int monthDayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
	if (month == 2 && isLeapYear(year))
	{
		return 29;
	}
	else
	{
		return monthDayArray[month];
	}
}


// d1 -= 100
Date& Date::operator-=(int day)
{
	if (day < 0)
		return *this += -day;

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

		_day += GetMonthDay(_year, _month);
	}

	return *this;
}

Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= -day;
	}
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		_month++;
		if (_month == 13)
		{
			_year++;
			_month = 1;
		}
	}
	return *this;
}
// d1 - d2
int Date::operator-(const Date& d)
{
	int flag = 1;
	Date max = *this;
	Date min = d;
	if (*this < d)
	{
		min = *this;
		max = d;
		flag = -1;
	}

	int n = 0;
	while (min != max)
	{
		++n;
		++min;
	}

	return n * flag;
}

大家如果需要菜单,自己去写吧。希望大家下来自己去实现一遍,只有消化了,知识才属于自己。

❗️8. 拓展:>> 和 <<

流提取(>>)和流插入(<<)运算符,这两个运算符想要重载就得引入一个东西。
我们知道内置类型是直接支持这两个运算符的,并且能自动识别类型,大家知道编译器是如何自动识别的吗?cin和cout又是什么东西呢?
cin和cout其实是全局的对象,它们包含在 <iostream>这个头文件里面,所以我们平时要包这个<iostream>头文件。cin是istream的对象,cout是ostream的对象。所以说istreamostream是两个类型,这两个类型是库里面的。
在这里插入图片描述
在这里插入图片描述
我们内置类型可以直接用,是因为C++已经帮你把内置类型重载了。自动识别类型本质上源自于函数重载。
我们是自定义类型,那我们也写一个。假设流插入,那这里就是ostream的对象,因为我们需要cout,out就是cout的别名。

void operator<<(std::ostream& out)
{
	out << _year << "-" << _month << "-" << _day << endl;
}

如果这样写,那就要d1 << cout;(d1.operator<<(cout);)这样调用,可是这样调用也太戳了,把cout流插入到d1里面?
因为cout << d1;这样调用等价于cout.operator<<(d1);这样调用。这里的<<流插入是作为cout的成员函数,只支持内置类型,不支持自定义类型。我们不可能把ostream类型的代码给改了,cout是库里面写的,我们不能更改。
我们知道,对于有两个操作数的运算符,左边的操作数是必然要传给this指针,我们动不了。那怎么办?有没有方式可以解决一下呢?
我们只要把ostream类型的cout对象作为第一个参数就成了,成员函数就没有机会了,那我们把它写成全局的。

void operator<<(std::ostream& out,const Date& d)
{
	out <<d. _year << "-" << d._month << "-" << d._day << endl;
}

但是这样又有一个问题:成员变量是私有的,我们访问不了。
我们可以用友元函数来解决,友元可以加在任意位置。

class Date
{
	// 友元函数
	friend std::ostream& operator<<(std::ostream& out, const Date& d);
	friend std::istream& operator>>(std::istream& out, Date& d);
public:
	bool isLeapYear(int year)
	{
		return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
	}

	int GetMonthDay(int year, int month);

	Date(int year = 1, int month = 1, int day = 1);
	
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

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

这样>>和<<的运算符重载函数就是这个类的朋友了,就可以访问私有的成员变量了。

cout << d1 << d2;
operator<<(cout, d1);

但是这个<<流插入的全局函数不能定义在Date.h里面,因为Date.cpp和Test.cpp(假设是你们写的菜单文件)同时包含这个头文件就会链接冲突(因为两个cpp文件编译后就会产生两个.o文件,两个.o文件里的符号表都有这个函数)。所以全局函数一般不写在.h文件里。
类里面定义的函数之所以链接不冲突,是因为默认是内联函数,不会放进符号表的。
所以全局函数在.h文件里声明,在Date.cpp里定义。
还有一个坑哦:如果要支持连续流插入,函数就必须有一个返回值。 所以我们必须这样玩:

std::ostream& operator<<(std::ostream& out, const Date& d)
{
	out <<d. _year << "-" << d._month << "-" << d._day << endl;
	return out;
}

cin(>>)也同理:这个时候日期类就不用const修饰了,因为你提取到的值要放进d里。

std::ostream& operator>>(std::ostream& int, Date& d)
{
	in >>d. _year >> "-" >> d._month >> "-" >> d._day >> endl;
	return in;
}

流提取为了防止提取非法日期,还得加一个检查,这里我就不写了。

评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yuucho

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值