前言
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);
}
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()观察前置与后置运算的区别: