目录
- 前言
- 一、日期类的基本样式
- 二、构造函数
- 三、拷贝构造函数
- 四、赋值运算符重载
- 五、日期比较
- 1. 判断一个日期是否小于另一个日期
- 2. 判断一个日期和另一个日期是否相等
- 3. 判断一个日期是否小于等于另一个日期
- 4. 判断一个日期是否大于另一个日期
- 5. 判断一个日期是否大于等于另一个日期
- 六、日期+天数 = 天数后的日期
- 七、日期+=天数 = 天数后的日期
- 八、日期-天数 = 天数前的日期
- 九、日期-=天数 = 天数前的日期
- 十、日期的++ = 日期
- 1. 前置++
- 2. 后置++
- 十一、日期的-- = 日期
- 1. 前置--
- 2. 后置--
- 十二、日期-日期 = 相差天数
- 十三、日期的流插入和流提取运算
- 1、日期的流插入
- 2. 日期的流提取
- 十四、打印日期
前言
在我们的日常生活中都需要知道当天的日期,同时也可能需要知道几天后的日期,几天前的日期,还有就是距离我们想要的日期还剩几天,比如:距离高考还有几天这样的信息,所以日期类对于我们任何人来说都是一个非常重要的工具。今天这篇文章将详细介绍日期类中各种功能的实现以及细节。
首先需要建一个日期类的项目来实现我们日期类中的全部代码:
一、日期类的基本样式
我们实现的日期类中主要包含的成员变量属性有:年、月、日,并且其中可能包含各种功能(函数):构造函数,打印日期,拷贝构造函数,赋值运算符重载函数,其他的运算符重载函数。具体下面代码基于说明:
- 头文件中的代码
// 头文件的包含和命名空间的展开
#include <iostream>
using std::endl;
using std::cout;
using std::cin;
class Date
{
public:
// 头文件中需要包含函数的声明:
// 判断闰年
bool isLeapYear(int year) const;
// 求每个月的天数
int GetMonthDay(int year, int month) const;
// 构造函数
Date(int year = 1, int month = 1, int day = 1);
// 拷贝构造函数
Date(const Date& d);
// 赋值运算符重载
Date& operator=(const Date& d);
// 比较日期
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;
// 日期+天数 = 日期
Date operator+(int day) const;
// 日期+=天数 = 日期
Date& operator+=(int day);
// 日期-天数 = 日期
Date operator-(int day) const;
// 日期-=天数 = 日期
Date& operator-=(int day);
// 日期++ = 日期
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 日期-- = 日期
// 前置--
Date& operator--();
// 后置--
Date operator--(int);
// 日期-日期 = 相差天数
int operator-(const Date& d) const;
// 打印日期
void Print()const;
// 使用流插入将日期进行输出
friend std::ostream& operator<<(std::ostream& out, const Date& d);
// 使用流提取获取日期
friend std::istream& operator>>(std::istream& in, Date& d);
private:
// 成员变量
int _year;
int _month;
int _day;
};
上述代码中需要注意几个细节,因为我们今天实现的是一个日期类的项目,我们我们代码 的书写需要规范一点,平时我们自己在练习写代码的时候通常都是将std整个命名空间的内容全部展开,这样其实是不规范的,今天我们写项目就是需要使用什么就将什么展开,尽量展开需要经常使用的东西。
- 上面的函数声明中,有的函数在最后会加上const关键字,有点函数却没有,这是为啥?
这个涉及的是 const成员函数的知识点,我们知道,在类的成员函数中会隐藏一个指针:this,这个指针的类型是Date* const ,也就是Date* const this,这个const修饰的是this,也就是限制this不能被改变(可以初始化),但是this指向的对象是可以发生改变的,为了防止一些不需要修改调用成员函数的对象的函数中误操作对对象进行修改,我们考虑对this进行保护,也就是在this前加上const,本来应该是:const Date* const this,但是我们知道,this指针是编译器处理的,我们通常不需要显示处理,所以这个const就放在了括号外,也就是上面这种样子。
下面源文件中的代码需要定义各个函数的具体细节,以下会分函数进行讲解。
二、构造函数
- 基本代码
// 判断闰年
bool Date::isLeapYear(int year) const
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
// 求每个月的天数
int Date::GetMonthDay(int year, int month) const
{
int arr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
if (isLeapYear(year) && month == 2)
{
return 29;
}
else
{
return arr[month];
}
}
// 构造函数
Date::Date(int year, int month, int day)
{
if (year > 0 && month > 0 && month < 13 && day>0 && day < GetMonthDay(year, month))
{
_year = year;
_month = month;
_day = day;
}
else
{
cout << "输入的如期不合法" << endl;
}
}
分析:在实现构造函数的时候,我们不能直接将传进来的参数直接对对象中的成员变量进行初始化,因为用户传进来的数值可能是不合法的,所以我们需要对其进行判断,判断的原则:年大于0,月在1到12,天数大于0,但是需要小于等于当前月的最大天数。显然我们需要一个函数去求解每一年中每一个月的天数,实现这个函数中显然需要考虑平年和闰年的问题,我们首先考虑常规情况,也就是假设为平年,将每一年的天数存放到一个数组中,这个数组在实现的时候也有一个技巧,我们多开一个空间,因为这样的话,传进来的是哪一个月,那么其天数就算数组中对应下标的位置存储的值,然后再考虑特殊情况,也就算闰年的情况,如果是闰年,并且是2月,那么天数就应该是29。实现闰年的时候就是一个逻辑:4年一闰,百年不闰,400年再闰一次。
三、拷贝构造函数
对于日期类,因为类中不存在动态开辟的资源,因此浅拷贝就能够实现对象的拷贝构造,我们不写编译器自动生成的拷贝构造函数就能够实现日期类的浅拷贝功能,但是为了练习,我们今天还是实现以下日期类的拷贝构造函数。代码如下:
// 拷贝构造函数
Date::Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
拷贝构造函数实现中需要注意一个细节:参数必须采用引用传参,否则会引发无穷递归,同时如果不会改变被拷贝的对象,需要加上const进行修饰,防止被拷贝对象被修改。
四、赋值运算符重载
// 赋值运算符重载
Date& Date::operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
赋值运算符重载函数中需要注意:
- 参数:传递的参数是一个自定义类型,是一次拷贝,我们可以实现成传引用参数,这样可以减少一次拷贝,较少消耗。
- 赋值自己:赋值前检查是否为自己给自己赋值,通过this和d的地址进行判断,如果是则不需要进行赋值操作
- 返回值:赋值运算通常是支持连续赋值的,所以一次赋值后的结果应该是上一次赋值后的左值。
五、日期比较
1. 判断一个日期是否小于另一个日期
// 比较日期
bool Date::operator<(const Date& d) const
{
return (_year < d._year) ||
(_year == d._year && _month < d._month) ||
(_year == d._year && _month == d._month && _day < d._day);
}
判断的逻辑:
- 如果年小,则小于
- 如果年相等,则看月,月小则小于
- 如果年相等,月相等,则看天,天小则小于
2. 判断一个日期和另一个日期是否相等
bool Date::operator==(const Date& d) const
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
判断两个日期是否相等的依据是:两个日期的年月日分别对应相等。
3. 判断一个日期是否小于等于另一个日期
bool Date::operator<=(const Date& d) const
{
return *this < d || *this == d;
}
这个函数可以采用复用的原则,因为一个日期小于等于另一个日期的情况就是:一个日期小于另一个日期,一个日期等于另一个日期。
4. 判断一个日期是否大于另一个日期
bool Date::operator>(const Date& d) const
{
return !(*this <= d);
}
这个函数同样可以采用复用,通过逻辑反操作来进行实现,因为我们要判断一个日期是否大于另一个日期,我们可以通过相反的情况进行分析:如果这个日期小于等于另一个日期,那么这个日期肯定不大于另一个日期,用代码的角度来进行解释的话:如果一个日期小于等于另一个日期,那么小于等于的判断就为真,但是实际上这个是不大于的,所以在前面的基础上再加上逻辑反操作即可。
5. 判断一个日期是否大于等于另一个日期
bool Date::operator>=(const Date& d) const
{
return *this > d || *this == d;
采用复用即可实现
六、日期+天数 = 天数后的日期
- 思路1
// 思路1:
Date Date::operator+(int day) const
{
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 == 0)
{
ret._month = 12;
--ret._year;
}
}
return ret;
}
思想:首先要知道的是,日期+天数是不会改变原有的日期的,所以我们需要利用原来的日期来拷贝构造一个一模一样的日期,然后再在拷贝出来的日期进行处理。首先,直接将day加到拷贝出来的日期,加上之后的日期中的天数可能会超出当前月的天数,那么就显然需要进行调整,这时就需要加上当前月的天数,然后月数加1,在月数++的过程中可能会出现月数超过12的情况,那么这个时候需要对月数进行处理,也就是到下一年的情况了,当日期中的天数没有超出当前月的天数时此时日期就是合法的,也就是最终的结果。
- 思路2
// 思路2
Date Date::operator+(int day) const
{
Date ret(*this);
while (day--)
{
ret._day++;
if (ret._day > GetMonthDay(ret._year, ret._month))
{
ret._day = 1;
ret._month++;
}
if (ret._month == 13)
{
ret._month = 1;
ret._year++;
}
}
return ret;
}
思想:思想与第一种思想有所不同,这个思路的思想是要求几天后,那么就循环将日期增加几次,其中的月数和闰年的情况全部在GetMonthDay(year,month)
函数中会考虑,在增加的过程中出现的临界:
- 天数增加到大于当前月的最大值:日期变成下一个月的1号,调整:月数++,天数变成1
- 月数增加到大于12:跨年,月份变成下一年的一月,调整:年份++,月份变成1月
七、日期+=天数 = 天数后的日期
- 思路1
// 思路1:复用
Date& Date::operator+=(int day)
{
*this = *this + day;
return *this;
}
- 思路2
// 思路2:
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
_month++;
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
- 思路3
// 思路3:
Date& Date::operator+=(int day)
{
while (day--)
{
_day++;
if (_day > GetMonthDay(_year, _month))
{
_day = 1;
_month++;
}
if (_month == 13)
{
_month = 1;
_year++;
}
}
return *this;
}
八、日期-天数 = 天数前的日期
- 思路1
// 日期-天数 = 日期
Date Date::operator-(int day) const
{
Date ret(*this);
ret._day -= day;
while (ret._day <= 0)
{
ret._month--;
if (ret._month == 0)
{
ret._month = 12;
ret._year--;
}
ret._day += GetMonthDay(ret._year, ret._month);
}
return ret;
}
思想:和加法一样,原来的日期是不会变的,所以需要拷贝一个临时对象进行处理。先将原来日期中的天数减小day,减小之后的日期中的天数可能会出现小于等于0,那么显然是不合法的,所以就需要进行处理。当日期中的天数是小于等于0时,此时需要上一个月的天数来进行调整,有点类似于借位,所以需要先月份–,退到上一个月,退到上一个月可能会出现跨年的情况,所以需要进行判断,如果月份减小到0,那么此时就是需要到上一年的12月,也就是年份–,月份等于12,当月份没问题时,求上一个月的天数,加到日期的天数。重复上述逻辑,知道日期的天数大于0时,日期合法,退出循环。
- 思路2
// 思路2
Date Date::operator-(int day) const
{
Date ret(*this);
while (day--)
{
ret._day--;
if (ret._day == 0)
{
ret._month--;
if (ret._month == 0)
{
ret._month = 12;
ret._year--;
}
ret._day = GetMonthDay(ret._year, ret._month);
}
}
return ret;
}
思想:减少几天,那么日期就向前退几天,那么循环就循环几次,在循环的处理过程中,同样可能出现一些边界需要处理。当日期的天数减小到0时,此时需要调整到上一个月的最后一天,当月份减小到0时,此时需要调整到上一年的最后一个月。
九、日期-=天数 = 天数前的日期
- 思路1
// 思路1:复用
Date& Date::operator-=(int day)
{
*this = *this - day;
return *this;
}
- 思路2
// 思路2
Date& Date::operator-=(int day)
{
while (day--)
{
_day--;
if (_day == 0)
{
--_month;
if (_month == 0)
{
_month = 12;
_year--;
}
_day = GetMonthDay(_year, _month);
}
}
return *this;
}
- 思路3
// 思路3
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
_month--;
if (_month == 0)
{
_year--;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
十、日期的++ = 日期
1. 前置++
// 前置++
Date& Date::operator++()
{
*this = *this + 1;
return *this;
}
前置++:先++,再返回,返回的是++后的日期,所以直接对原来的日期进行处理即可,出了函数,返回的日期仍然存在,所以返回值用引用。
2. 后置++
// 后置++
Date Date::operator++(int)
{
Date ret(*this);
*this = *this + 1;
return ret;
}
后置++:先返回,后++,返回的是++前的日期,所以需要先保存++前的值,再处理原来的日期,然后将此临时对象返回,返回的临时对象显然在函数结束后是不存在的,所以不能用引用返回,只能传值返回。
十一、日期的-- = 日期
分析方法与++一致!
1. 前置–
// 前置--
Date& Date::operator--()
{
*this = *this - 1;
return *this;
}
2. 后置–
// 后置--
Date Date::operator--(int)
{
Date ret(*this);
*this = *this - 1;
return *this;
}
十二、日期-日期 = 相差天数
// 日期-日期 = 相差天数
int Date::operator-(const Date& d) const
{
Date max = *this;
Date min = d;
int flag = 1;
int count = 0;
if (*this < d)
{
max = d;
min = *this;
flag = -1;
}
while (min < max)
{
min++;
count++;
}
return flag * count;
}
思想:本质就是一个追击相遇问题,先确定两个日期中的大日期和小日期,显然,如果是大日期-小日期,结果是正数。如果小日期-大日期,结果是负数。
确定方法:假设第一个日期(this)是大日期,第二个日期是小日期(d),最终this-d>0,此时设置一个标记位1记录结果的符号。如果上述假设不成立,那么就是第一个日期(this)是小的,第二个日期是大日期,结果是负数,即this-d<0。然后使用一个循环让小日期去追击大日期,当小日期还没有到大日期时,小日期的天数+1,知道小日期追上大日期时结束循环。
十三、日期的流插入和流提取运算
这个函数不能声明为成员函数,因为如果声明为成员函数,那么第一个参数就默认是日期,第二个参数才是cout了,这样的顺序显然是不对的,所以这个函数不能是类的成员函数。
通过上面的分析,这个函数只能是类外的函数,但是类外的函数又会遇到一个问题:不能访问类中的私有成员,此时就需要将这个函数设计成这个类的友元函数,设计方法:在类中使用friend关键字声明该函数,在对应的源文件中定义该函数,注意不能在头文件中定义该函数,否则在链接的时候会出现重定义。
1、日期的流插入
std::ostream& operator<<(std::ostream& out, const Date& d)
{
out << d._year << "-" << d._month << "-" << d._day << endl;
return out;
}
上述的函数需要实现的就是将日期输出到显示器上,显然日期类是一个自定义类型,cout并不知道要以怎样的形式将日期输出,所以这个时候需要采用运算符重载重载函数中需要注意:
- 参数:第一个参数是cout的别名,第二个参数是输出日期的别名。函数中只需要输出日期,并不会对日期进行修改,所以参数中日期前可以加上const关键字,防止输出日期被修改。
- 返回值:因为流插入输出时是支持连续输出的,所以这个运算符在重载的时候是需要通过控制函数返回值来实现连续输出的,返回的显然是ostream类型的对象,函数结束后,对象仍然存在,所以需要传引用返回。
2. 日期的流提取
std::istream& operator>>(std::istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
这个函数的功能主要是从流中提取日期的内容,在实现的时候,需要注意以下方面:
- 参数:第一个参数是cin的别名,第二个参数是待处理日期的别名,注意这个日期不能加上const关键字进行修饰,因为我们在函数中是需要对这个日期的类型进行修改的。
- 返回值:和<<运算符重载一样的道理。
十四、打印日期
// 打印日期
void Print()const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
当我们没有实现流插入运算符重载时i,我们可以考虑通过实现这个成员函数将对象中的内容输出,但是通过这样的方法代码的可读性就比较低了。