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

  • The pessimist complains about the wind; the optimist expects it to change; the realist adjusts the sails.
    • 悲观主义者抱怨风;乐观主义者期望它改变;现实主义者调整风帆。


文章目录

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

️前言

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


️正文

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

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

Date.h

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

Date.cpp

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

test.cpp

类的定义

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

//当前位于文件 Date.h 中#pragma once#includeusing 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;elsereturn 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;elsereturn 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;elsereturn 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 += -100if (val < 0){//此时需要调用 -=*this -= (-val);return *this;}//因为是 += 不需要创建临时对象//首先把天数全部加至 _day 上_day += val;//获取当前月份天数int monthDay = getMonthDay();//判断进位,直至 _day <= monthDaywhile (_day > monthDay){//此时大于,先把多余的天数减掉_day -= monthDay;//此时进位一个月++_month;//判断月份是否大于 12if (_month > 12){//此时需要进年++_year;//月份变为1月_month = 1;//判断是否为0年if (_year == 0)_year = 1;//调整}//重新获取月份天数monthDay = getMonthDay();}//返回 *this 本身return *this;}

有了这个函数后,我们就可以根据当前日期推算 N 天后的日期

日期+天数 可以直接复用上面的代码,而 日期-=天数 将逻辑反过来就行了,这里不展示代码了,完整代码在文末的 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


️总结

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

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

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


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

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

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