【C++】:日期类的实现 -- 日期计算器

简介: 【C++】:日期类的实现 -- 日期计算器

前言

1.日期类是一种十分经典的类型。对于C++的初学者,它能够帮助我们融会贯通许多C++的基础知识,它涉及许多的基础语法,比如引用,函数重载,传值/传参返回,构造函数,运算符重载,const成员等等。

如果有不了解的,可以前往我的主页浏览相关文章。

日期计算器可以实现两个日期的比较,两个日期的相减,日期的加减天数等有意义的运算。

2.本文依然采用多文件方式。其中:

Date.h //定义类,存放各函数的声明;

Date.cpp //实现各重载函数;

Test.cpp //测试各函数的功能。

在C++中,由于函数的声明与定义分离,如果要定义成员函数,就要指定类域,这是基本语法。

一,各个函数功能的实现

1. 检查输入的日期是否合法

不管是日期的比较还是日期的运算,第一步都要检查日期的合法性。特别是月份和每个月的天数

代码实现如下:

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

2. 构造函数 (初始化函数)

为了方便,在使用默认构造函数时,一般是自己显式的实现一个全缺省构造函数

注意:

在函数的声明和定义分离时,如果要给缺省值,必须在函数声明的时候给。

代码实现如下:

Date::Date(int year, int month, int day)
{
  _year = year;
  _month = month;
  _day = day;
     //日期的源头,是从构造函数里出来的,所以要在这里判断
  if (!CheakDate())
  {
    cout << "日期非法!" << endl;
  }
}

二,比较类的运算符重载

3. <运算符重载

判断两个日期谁更小。思路:先比年,年小就小,年相等比月,月小就小,年月相等比日,日小就小。

代码实现如下:

d1 < d2 隐含的this指针是d1,d是d2的别名

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)
    {
      return _day < d._day;
    }
  }
  return false;
}

4. ==运算符重载

判断两个日期是否相等 。这个比较简单,如果两者的年月日都相等,即相等。

代码实现如下:

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

5. >=运算符重载

有人可能会仿照<运算符重载的方法,使用复杂的逻辑,写各种晦涩的代码实现。其实只要实现了<运算符重载和==运算符重载,下面的日期比较类都是可以复用的。 比如这里的>=,< 取反就是>=

代码实现如下:

bool Date::operator>= (const Date& d) const
{
  return !(*this < d);
}
• 1
• 2
• 3
• 4

6. >运算符重载

<= 取反,就是>。

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

7. <=运算符重载

只要满足<或者=,就是<=。

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

8. !=运算符重载

==去取反,就是!=

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

9. 获取某月的天数

这个函数是整个日期类的关键,也是最频繁调用的一个函数。由于这个原因,最好把它定义成内联函数,避免每次调用都要开辟空间,可以提升效率。根据C++的语法,定义在类里默认是内联,inline可加可不加

代码实现如下:

这里还有两个优化的细节:

1. month == 2 和后面的取模运算的位置。首先满足是2月,再判断是否是闰年,效率会更高。

2. static的使用,由于该函数频繁调用,把数组放在静态区,避免每次调用函数时每次都要开辟数组空间。

int GetMonthDay(int year, int month)
{
    //断言,确保输入月份的有效性
    assert(month > 0 && month < 13);
    //枚举出月份的天数
   static int monthDayArray[13] = { -1, 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;
    }
    else
    {
        return monthDayArray[month];
    }
}

三,运算类的重载

10. 日期+=天数

比如d1 + 50,这里的d1已经改变了

计算过程如下:

注意,每次超过月份天数后,是减当前月的天数。

代码实现如下:

Date& Date::operator+=(int day)
{
  //这里是处理有人传负的天数,转化成调用-=函数
  if (day < 0)
  {
    return *this -= -day;
  }
  //先加上天数
  _day += day;
  //加上天数后超出了月的范围
  while (_day > GetMonthDay(_year, _month))
  {
    //减去当前月的天数,此时月份+1
    _day -= GetMonthDay(_year, _month);
    ++_month;
    //超过12个月时
    if (_month == 13)
    {
      ++_year;//年份+1
      _month = 1;//别忘了月份还要从1月开始
    }
  }
  return *this;
}

11. 日期 + 天数

比如 d1 + 50,d1没有改变。

代码实现如下:

Date Date::operator+(int day) const
{
    //实例化一个临时的局部变量,用拷贝构造
    //把d1的日期拷贝给tmp,这样d1就不会改变
  Date tmp = *this;
  tmp += day;//直接复用+=
  //注意:出了这个函数,tmp会被销毁,所以这里不能用引用返回。
  //      这里是传值返回,所以会形成一个拷贝
  return tmp;
}

12. 日期-=天数

比如 d1- 50,这里的 d1也改变了。

计算过程如下:

注意,这里加(借)的是下一个月的天数。

代码实现如下:

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;

13. 日期 - 天数

思路同 日期 + 天数。

代码实现如下:

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

四,前置,后置类的重载

首先要知道前置和后置运算的区别:

前置:返回运算后的值

后置:返回运算前的值

其次,还要理解函数重载和运算符的重载:

函数重载:可以让函数名相同,参数不同的函数存在;

运算符重载:让自定义类型可以用运算符,并且控制运算符的行为,增强可读性

这两者各论各的,没有关系。但是,多个同一运算符重载可以构成函数重载。

我们知道,前置和后置是同一运算的不同形式 ,但是他们的函数名相同,参数都是隐含的this参数,无法构成重载同时存在。所以为了区分,并且构成重载,C++规定:强行给后置(后置++和后置- -)函数的参数增加了一个 int 形参,不需要写形参名。并且这个形参没有任何意义。

14. 前置++

前置++先加,后用,返回的是加之后的值,可以直接复用+=运算符重载,返回的是改变后的 *this。

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

15. 后置++

注意:后置函数多一个形参 int,以便与前置构成重载。

后置++是先用,后加,返回的是加之前的值。所以需要创建一个临时的局部对象 tmp,用拷贝构造把原来的 *this 拷贝给 tmp 。最后返回 tmp。

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

16. 前置 - -

前置- -和前置++的原理类似。

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

17. 后置 - -

注意:后置函数多一个形参 int,以便与前置构成重载。

原理与后置++类似。

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

注意:

  • 前置和后置运算,一般建议用前置,因为后置类需要拷贝构造,传值返回,这就会产生两次拷贝和一次析构,而前置却没有这样的消耗,相比之下前置类有优势

18. 日期-日期 返回天数

两个日期相减,返回的是相差的天数,是一个整形。

思路:找出大的年份和小的年份,再定义一个计数器和小的年份一起++,直到和大的年份相等。

比如 d1 - d2

代码实现如下:

//隐含的this指针是d1,d是d2的别名
int Date::operator-(const Date& d) const
{
   //先假设大日期和小日期
  Date max = *this;
  Date min = d;
  
  //默认假设正确
  int flag = 1;
    
    //如果假设错误,就进行改正
  if (*this < d)
  {
    max = d;
    min = *this;
    flag = -1;
  }
  int n = 0;
  //让计数n和小的日期一起加,加到和大的日期相等
  while (min != max)
  {
    ++min;
    ++n;
  }
    
    //flag的正负有妙用
  return n * flag;
}

五,完整代码

Date.h

#pragma once
#include <iostream>
using namespace std;
#include <assert.h>
#include <stdbool.h>
    
class Date
{
     //构造函数
    Date(int year, int month, int day);
    void Print() const;
     
     //定义为内联函数
    int GetMonthDay(int year, int month)
    {
        //断言,确保输入月份的有效性
       assert(month > 0 && month < 13);
       static int monthDayArray[13] = { -1, 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;
        }
        else
        {
            return monthDayArray[month];
        }
    }
    //检查日期的合法性
    bool CheakDate();
    //两个日期之间的比较
    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;
    //d1 += 100,d1已经改变
    Date& operator+=(int day);
    Date& operator-=(int day);
    //d1 + 50,d1不变
    Date operator+(int day) const;
    Date operator-(int day) const;
    // d1 - d2
    int operator-(const Date& d) const;
    // ++d1 -> d1.operator++()
    Date& operator++();
    // d1++ -> d1.operator++(1) 整数任意给
    Date operator++(int);
    //前置,后置--
    Date& operator--();
    Date operator--(int);
private:
    int _year;
    int _month;
    int _day;
};

Date.cpp

#define _CRT_SECURE_NO_WARNINGS 
#include "Date.h"
bool Date::CheakDate()
{
  if (_month < 1 || _month>12
    || _day<1 || _day>GetMonthDay(_year, _month))
  {
    return false;
  }
  else
  {
    return true;
  }
}
//1.缺省参数只能在声明的时候给
//2.成员函数声明与定义分离时,要指定类域
Date::Date(int year, int month, int day)
{
  _year = year;
  _month = month;
  _day = day;
  if (!CheakDate())
  {
    cout << "日期非法!" << endl;
  }
}
void Date::Print() const
{
  cout << _year << "-" << _month << "-" << _day << endl;
}
//思路:先比年,年小就小,年相等比月,月小就小,年月相等比日,日小就小
//d1 <d2 隐含的this是d1, d是d2的别名
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)
    {
      return _day < d._day;
    }
  }
  return false;
}
//先写好大于和等于 或者 小于和等于的函数,其余的进行复用
//d1 <=d2
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 < d);
}
bool  Date::operator==(const Date& d) const
{
  return _year == d._year
    && _month == d._month
    && _day == d._day;
}
bool  Date::operator!=(const Date& d) const
{
  return !(*this == d);
}
//日期 += 天数 :d1 + 100
//这里的d1 已经改了
Date& Date::operator+=(int day)
{
  //这里是处理有人传负的天数
  if (day < 0)
  {
    return *this -= -day;
  }
  //先加上天数
  _day += day;
  //加上天数后超出了月的范围
  while (_day > GetMonthDay(_year, _month))
  {
    //减去当前月的天数,此时月份+1
    _day -= GetMonthDay(_year, _month);
    ++_month;
    //超过12个月时
    if (_month == 13)
    {
      ++_year;//年份+1
      _month = 1;//别忘了月份还要从1月开始
    }
  }
  return *this;
}
// d1 + 50,d1没有改变
Date Date::operator+(int day) const
{
    //实例化一个临时的局部变量,用拷贝构造,把d1的日期拷贝给tmp,这样d1就不会改变
  Date tmp = *this;
  tmp += day;//直接复用+=
  //注意:出了这个函数,tmp会被销毁,所以这里不能用引用返回。
  //      这里是传值返回,所以会形成一个拷贝
  return tmp;
}
//日期 -= 天数 :d1 - 100
//这里的d1 已经改了
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) const
{
  Date tmp = *this;
  tmp -= day;
  return tmp;
}
// ++d
Date& Date::operator++()
{
  *this += 1;
  return *this;
}
//这两种++ 建议用前置++,因为后置++会产生两次拷贝和一次析构,相比之下前置++有优势。
//d++
Date Date::operator++(int)
{
  Date tmp = *this;
  *this += 1;
  return tmp;
}
//--d
Date& Date::operator--()
{
  *this -= 1;
  return *this;
}
//d--
Date Date::operator--(int) 
{
  Date tmp(*this);
  *this -= 1;
  return tmp;
}
//思路:找出大的年份和小的年份,再定义一个计数器和小的年份一起++,直到和大的年份相等。
//d1 - d2
int Date::operator-(const Date& d) const
{
  Date max = *this;
  Date min = d;
  int flag = 1;
  if (*this < d)
  {
    max = d;
    min = *this;
    flag = -1;
  }
  int n = 0;
  while (min != max)
  {
    ++min;
    ++n;
  }
  return n * flag;
}

Test.cpp

#define _CRT_SECURE_NO_WARNINGS 
#include "Date.h"
void TestDate1()
{
  
  Date d1(2024, 4, 14);
  Date d2 = d1 + 5000;
  d1.Print();
  d2.Print();
  Date d3(2024, 4, 14);
  Date d4 = d3 - 5000;
  d3.Print();
  d4.Print();
  Date d5 (2024, 4, 14);
  d5 += -5000;//转化成-=运算,计算5000天之前的时间
  d5.Print();
}
void TestDate2()
{
  Date d1(2024, 4, 14);
  Date d2 = ++d1;
  d1.Print();
  d2.Print();
  Date d3 = d1++;
  d1.Print();
  d3.Print();
}
int main()
{
  TestDate1();
  return 0;
}

日期的比较类比较简单,不在这里示范,读者自行验证。

比如,调用TestDate1()计算未来的日期和以前的日期:

再比如,调用TestDate2()观察前置与后置运算的区别:

目录
相关文章
|
24天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
38 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
80 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
86 4
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
31 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
31 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
26 1
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
2月前
|
存储 编译器 C语言
【C++类和对象(上)】—— 我与C++的不解之缘(三)
【C++类和对象(上)】—— 我与C++的不解之缘(三)