类和对象实操之【日期类】

✨个人主页: Yohifo
所属专栏: C++修行之路
每篇一句: 图片来源

  • The pessimist complains about the wind; the optimist expects it to change; the realist adjusts the sails.

    • 悲观主义者抱怨风;乐观主义者期望它改变;现实主义者调整风帆。

    类和对象实操之【日期类】_第1张图片


文章目录

  • ️前言
  • ️正文
    • 类的定义
      • 合法性检验
      • 判断闰年
      • 获取年份天数
      • 获取月份天数
    • 运算符重载
      • 判断等于
      • 判断小于
      • 复用至所有判断
      • 重载流插入、提取
    • 日期+=天数
      • 核心思想
      • 代码实现
    • 日期-日期
      • 核心思想
      • 代码实现
    • 自加、自减操作
      • 前置
      • 后置
    • 程序源码
  • ️总结


️前言

在学完类和对象相关知识后,需要一个程序来供我们练习、巩固知识点,日期类就是我们练习的首选程序,日期类实现简单且功能丰富,相信在完整地将日期类实现后,能对类和对象有更好的掌握及更深的理解

类和对象实操之【日期类】_第2张图片


️正文

为了更符合工程标准,这里采用三个文件的方式实现程序

用于声明类和方法的 .h 头文件

Date.h

用于实现类和方法的 .cpp 源文件

Date.cpp

用于测试功能的 .cpp 源文件

test.cpp

工程框架

类的定义

先简单定义一下每个类中都有的默认成员函数

//当前位于文件 Date.h 中
#pragma once
#include
using std::cout;	//采用部分展开的方式
using std::cin;

//采用命名空间
namespace Yohifo
{
	class Date
	{
	public:
		//构造函数,频繁使用且短小的代码直接在类的声明中实现,成为内联函数
		Date(int year = 2023, int month = 2, int day = 11)
			:_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)
				return *this;

			_year = d._year;
			_month = d._month;
			_day = d._day;
			return *this;
		}

		//析构函数
		~Date()
		{
			_year = 1970;
			_month = 2;
			_day = 11;
		}

	private:
		int _year;	//年、月、日
		int _month;
		int _day;
	};
}

合法性检验

首先编写第一个函数:合法性检验

检验标准

  • 年不能为0
  • 月在区间 [1, 12] 内,超过为非法
  • 根据年月推算出天数,天数不能操作规定天数,也不能 <= 0

注意:

  • 当前包括后续函数都是采取先在头文件 Date.h 的类中声明,再到 Date.cpp 实现的路径
  • 因历史原因导致的闰年变动这里不考虑,该程序实现的是理想情况下的闰年状态
  • 程序计算范围覆盖至公元前,限度为 [INT_MIN, INT_MAX]
#include"Date.h"

using namespace Yohifo;	//全局展开命名空间

//合法性检验
bool Date::check() const
{
	//年不能为0年
	if (_year == 0)
		return false;

	//月份区间 [1, 12]
	if (_month < 1 || _month > 12)
		return false;

	//天数要合理
	// getMonthDay 函数后续实现
	if (_day < 1 || _day > getMonthDay())
		return false;

	return true;
}

判断闰年

闰年二月多一天,因此需要特殊处理

闰年判断技巧: 四年一闰且百年不闰 或者 四百年一闰

//闰年判断
bool Date::checkLeapYear() const
{
	//按照技巧判断
	if (((_year % 4 == 0) && (_year % 100 != 0)) || (_year % 400 == 0))
		return true;
	else
		return false;
}

获取年份天数

闰年多一天,为 366 ,非闰年为 365,判断返回即可

//获取年份天数
int Date::getYearDay() const
{
	//复用代码
	return (checkLeapYear() ? 366 : 365);
}

获取月份天数

根据当前年份和月份,判断当月有多少天

注意: 闰年的二月需要特殊处理

//获取月份天数
int Date::getMonthDay() const
{
	//非闰年情况下每个月天数,其中下标代表月份
	int arr[] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

	//如果为2月,且为闰年,直接返回 2月+1天
	if (_month == 2 && checkLeapYear())
		return arr[_month] + 1;
	else
		return arr[_month];
}

运算符重载

前面学习了 operator 运算符重载,现在正好可以拿来练练手

判断等于

两个日期相等的前提是 都相等

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

判断小于

注意: 我们的运算顺序都是 左操作数右操作数,其中隐含的 this 指针默认为 左操作数

*this 小于 d 的逻辑

  • 首选判断年是否小于
  • 年相等,判断月是否小于
  • 年相等,月相等,判断天是否小于
//判断小于
bool Date::operator<(const Date& d) const
{
	if (_year < d._year)	//判断年
		return true;
	else if (_year == d._year && _month < d._month)	//判断月
		return true;
	else if (_year == d._year && _month == d._month && _day < d._day)	//判断天
		return true;
	else
		return false;
}

复用至所有判断

善用代码复用,有了等于和小于,我们可以直接写出所有判断

//判断不等于
bool Date::operator!=(const Date& d) const
{
	return !(*this == d);	//等于,取反为不等于
}

//判断小于等于
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) || (*this == d));
}

重载流插入、提取

coutcin 只能输出、输出内置类型,但如果我们对它进行改造一下,就能直接输出我们的自定义类型

注意:

  • cout 类型为 ostreamcin 类型为 istream
  • 要使得 coutcin 变为重载后的左操作数,此时的运算符重载就不能写在类内,因为在类中的函数默认 this 为第一个参数,即左操作数
  • 因此这两个函数比较特殊,需要写在外面,但同时又得访问类中的成员,此时就需要 友元函数
  • 两个函数都有返回值,返回的就是coutcin本身,避免出现 cout << d1 << d2 这种情况

此时可以利用合法性检验了

实现 operator>> 时,右操作数不能用 const 修饰

//在 Date.h 内
//新增几个局部展开
using std::ostream;
using std::istream;
using std::endl;

namespace Yohifo
{
	class Date
	{
		//声明为类的友元函数
		friend std::ostream& operator<<(std::ostream& out, const Date& d2);
		friend std::istream& operator>>(std::istream& in, Date& d2);	//注意
		
		//……
	};

	//直接定义在头文件中,成为内联函数
	inline ostream& operator<<(ostream& out, const Date& d)
	{
		//此时需要检验日期合法性
		if (Date(d).check() == false)
		{
			out << "警告,当前日期非法!" << endl;
			out << "后续操作将会受到限制" << endl;
		}

		out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
		return out;
	}

	inline istream& operator>>(istream& in, Date& d)
	{
		Date tmp;

flag:
		cout << "请入日期,格式为:年 月 日" << endl;
		in >> tmp._year >> tmp._month >> tmp._day;

		//如果输入日期非法,就重新输入
		if (Date(tmp).check() == false)
		{
			cout << "警告,当前日期非法!" << endl;
			cout << "日期输入失败,请尝试重新输入!" << endl;
			goto flag;
		}

		cout << "输入成功!" << endl;

		return in;
	}
}

有了这两个运算符重载后,我们就可以直接对自定义类型(日期类对象)直接进行输入输出操作了

Date d1;
cin >> d1;	//对自定义类型的输入
cout << d1;	//对自定义类型的输出

日期+=天数

下面涉及两个重要算法

  • 日期 += 天数
  • 日期 -= 天数

这里把 日期 += 天数 介绍清楚了,日期 -= 天数 就很好写了,就是倒着走

  • 有了 日期 += 天数 后,可以直接实现 日期 + 天数
  • 同理也可以实现 日期 - 天数

核心思想

注:此时实现的是 日期+=天数

进位思想:天数满了后进位到月份上,月份满后进位至年份上

注意:

  • 每个月对应天数都需要计算,因为每个月都不同
  • 月份为12月时,再+就变成了下一年的一月
  • 假设为公元前,加至0年时,需要特殊处理为公元1年
  • += 操作返回的是左操作数本身,应对 (d1 += 10) = 20 这种情况

代码实现

//日期+=天数
Date& Date::operator+=(const int val)
{
	if (check() == false)
	{
		cout << "警告,当前日期非法,无法进行操作" << endl;
		return *this;
	}
	
	//判断 val,避免恶意操作,如 d1 += -100
	if (val < 0)
	{
		//此时需要调用 -=
		*this -= (-val);
		return *this;
	}

	//因为是 += 不需要创建临时对象

	//首先把天数全部加至 _day 上
	_day += val;

	//获取当前月份天数
	int monthDay = getMonthDay();

	//判断进位,直至 _day <= monthDay
	while (_day > monthDay)
	{
		//此时大于,先把多余的天数减掉
		_day -= monthDay;

		//此时进位一个月
		++_month;

		//判断月份是否大于 12
		if (_month > 12)
		{
			//此时需要进年
			++_year;

			//月份变为1月
			_month = 1;

			//判断是否为0年
			if (_year == 0)
				_year = 1;	//调整
		}

		//重新获取月份天数
		monthDay = getMonthDay();
	}

	//返回 *this 本身
	return *this;
}

有了这个函数后,我们就可以根据当前日期推算 N 天后的日期
类和对象实操之【日期类】_第3张图片

日期+天数 可以直接复用上面的代码,而 日期-=天数 将逻辑反过来就行了,这里不展示代码了,完整代码在文末的 gitee 仓库中


日期-日期

日期+日期无意义,但日期-日期有,可以计算两日期差值

日期相减有两种情况:

  • 左操作数小于右操作数,此时返回大于0的值
  • 左操作数大于右操作数,此时返回小于0的值

具体实现时也很好处理,直接用一个 flag 就行了

核心思想

先不管左右操作数大小,我们先找出较大操作数与较小操作数

通过较小操作数逐渐逼近较大操作数,其中经过的天数就是差值

步骤:

  • 先把日期对齐,即小操作数日期与大操作数日期平齐
  • 再把月份对齐
  • 最后再把年份对齐就行了
  • 随着步骤的深入,天数计算会越来越快的

除了这种方法外,我们还可以直接一天一天的加,直到相等,当然这种效率较低

代码实现

//日期 - 日期
const int Date::operator-(const Date& d) const
{
	if (check() == false || d.check() == false)
	{
		cout << "警告,当前日期非法,无法进行操作!默认返回 0" << endl;
		return 0;
	}

	//假设右操作数为较大值
	Date max(d);
	Date min(*this);
	int flag = 1;

	//判断
	if (min > max)
	{
		max = *this;
		min = d;
		flag = -1;
	}

	//小的向大的靠近
	int daySum = 0;

	//考虑天
	while (min._day != max._day)
	{
		min += 1;
		daySum++;
	}

	//考虑月
	while (min._month != max._month)
	{
		daySum += min.getMonthDay();
		min += min.getMonthDay();
	}

	//考虑年
	while (min._year != max._year)
	{
		daySum += min.getYearDay();
		min._year++;
	}

	return daySum * flag;
}

这种方法(同轴转动)将会带来一定的性能提升(相对逐天相加来说)

方法 相差 1k 年 相差 1w 年 相差 10w 年
同轴转动 耗时 0 ms 耗时 0 ms 耗时 2 ms
逐天相加 耗时 28 ms 耗时 297 ms 耗时 3142 ms

注:实际差异与电脑性能有关


自加、自减操作

自加操作实现很简单,不过需要注意编译器是如何区分两者的

占位参数

  • 因为前置与后置的运算符重载函数名一致,此时需要给运算符多加一个参数以区分,这是由编译器规定的合法行为,占位参数加在后置运算符重载中

前置

前置直接复用前面 += 的代码

前置操作是先进行自加或自减,再返回

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

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

后置

此时需要借助 占位参数,当启用时,编译器会自动传参,并自动区分,占位参数 类型为 int

后置操作是先记录值,再进行自加或自减,返回之前记录的值

//后置++
const Date Date::operator++(int)
{
	//借助临时变量
	Date tmp(*this);

	*this += 1;

	return tmp;
}

//后置--
const Date Date::operator--(int)
{
	Date tmp(*this);

	*this -= 1;

	return tmp;
}

特别注意: 对于自定义类型来说,在进行自加、自减操作时,最好采用前置,因为后置会发生拷贝构造行为,造成资源浪费


程序源码

完整的代码在这里 Gitee
类和对象实操之【日期类】_第4张图片


️总结

以上就是关于日期类实现的全部内容了,涉及到了前面学的大部分知识,希望大家在看完后能把它独立敲一遍,加深理解

如果你觉得本文写的还不错的话,可以留下一个小小的赞,你的支持是我分享的最大动力!

如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正


星辰大海

相关文章推荐
类和对象合集系列
类和对象(下)
类和对象(中)
类和对象(上)

===============

C++入门必备
C++入门基础

感谢支持

你可能感兴趣的:(C++修行之路,c++,开发语言,学习,程序人生,类和对象)