C++实现日期类

Ⅰ. 实现日期类

0x00 引入
为了能够更好地讲解运算符重载的知识,我们将手把手地、一步一步地实现 "日期类" ,

因为通过日期类去讲解运算符重载是比较合适的。

日期类的拷贝构造、赋值、析构我们都可以不用写,让编译器自己生成就行了。

0x00 设计构造函数
规范一点,我们声明与定义分离开来。

Date.h

#include
using namespace std;

class Date {
public:
Date(int year = 1, int month = 1, int day = 1); // 全缺省构造
void Print() const; // 打印函数

private:
int _year;
int _month;
int _day;
};
Date.cpp

include "Date.h"

Date::Date(int year, int month, int day) {
this->_year = year;
this->_month = month;
this->_day = day;
}

void Date::Print() const {
printf("%d-%d-%d\n", this->_year, this->_month, this->_day);
}

int main(void)
{
Date d1;
d1.Print();

Date d2(2022, 3, 14);
d2.Print();

return 0;
}

运行结果如下:

构造函数我们用了全缺省,这样我们不给值的时候也可以打印默认的值。

通过打印函数我们有可以把日期打印出来,这里可以加 const 修饰,

上一篇我们说过,加 const 是很好的,只要不改变都建议加上 const 。

比如这里的 Print 是可以加 const 的,

而构造函数这里修改了成员变量,是不能加 const 的。

现在我们现在来思考一个特殊的问题……

如果,我是说如果!我们输入的日期是一个非法的日期呢?

比如:

Date d3(2022, 13, 15); // 作为地球人,怎么会有13月呢?
d3.Print();
运行结果如下:

如果有人输入了这种日期,还是能给他打印出来,

这合理吗?这不合理!

有人又觉得,谁会拿这种日期初始化啊,这种日期不是一眼就能看出来有问题嘛……

说得好,那这些呢?

int main(void)
{
Date d4(2022, 2, 29);
d4.Print();

Date d5(2009, 2, 29);
d5.Print();

Date d6(2000, 2, 29);
d6.Print();

return 0;
}
是不是没有那么容易一眼看出来了?这都涉及到闰年的问题了。

所以我们需要设计一个函数去判断,用户输入的日期到底合不合法。

Date.h

#include
using namespace std;

class Date {
public:
Date(int year = 1, int month = 1, int day = 1); // 全缺省构造
int GetMonthDay(int year, int month) const ; // 获取某年某月对应的天数
void Print() const; // 打印函数


private:
int _year;
int _month;
int _day;
};
因为每个月天数不统一,所以在写判断日期合法的算法前,

我们需要先设计一个可以获取一年中每个月对应天数的 GetMonthDay 函数。

如何设计呢?写12个 if else?

可以是可以,但是好像有点搓啊。

用 switch case 可能还好一点。我们这里可以写个简单的哈希来解决,

我们把每个月的天数放到一个数组里,为了方便还可以把下标为 0 的位置空出来,

这样我们就可以直接按照 "月份" 读出对应的 "日期" 了。

根据我们小时候背的口诀,依次把日期填到数组中就行了。

这里还要考虑到我们上面提到的闰年问题,闰年二月会多一天。

如果月份为 2,我们就进行判断,如果是闰年就让获取到的 day + 1 即可。

Date.c

int Date::GetMonthDay(int year, int month) {
static int monthDatArray[13] = { 0,
31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31
};
int day = monthDatArray[month]; // 获取天数

if (month == 2 // 先判断是否为二月
&& ((year % 4 == 0 && year % 100 != 0) // 是二月再判断是否是闰年
|| (year % 400 == 0))) {
day += 1; // 是闰年,天数+1
}

return day; // 返回计算的天数
}

解读:

创建一个 day 去获取月份对应的天数,然后进行判断。

如果传入的月份是2月,就判断是否是闰年。

这里的一个小细节就是我们是先判断传入的月份2月的,

如果不是2月我们是压根没有必要进行闰年判断的,

根据 && 的特性,碰到假后面后不会判断了,所以我们先判断传入的月份是否是2月,

是2月 —— 为真,再继续判断是否是闰年。

如果是闰年,让天数+1,就完事了。

有了GetMonthDay 函数,就解决了每个月天数不统一的问题了。

我们可以写判断部分了:

Date::Date(int year, int month, int day) {
this->_year = year;
this->_month = month;
this->_day = day;

// 判断日期是否合法
if ( ! (_year >= 0
&& (month > 0 && month < 13)
&& (day > 0 && _day <= GetMonthDay(year, month)))
) {
cout << "非法日期: ";
this->Print(); // 在类里面不仅仅可以访问成员变量,还可以访问成员函数(this可省略)
}
}
运行结果如下:

判断用户输入的日期是否合法的功能就写出来了,如果用户输入的日期不合法,

就把他铐起来! 我们就提示用户这是一个非法日期。

0x01 判断大于 operator>
比较两个日期的大小。

Date.h

#include
using namespace std;

class Date {
public:
Date(int year = 1, int month = 1, int day = 1); // 全缺省构造
int GetMonthDay(int year, int month) const ; // 获取某年某月对应的天数
void Print() const; // 打印函数
bool operator>(const Date& d) const; // d1 > d2

private:
int _year;
int _month;
int _day;
};

Date.cpp

/* d1 > d2 */
bool Date::operator>(const Date& d) const {
if (this->_year > d._year) {
return true;
}
else if (this->_year == d._year
&& this->_month > d._month) {
return true;
}
else if (this->_year == d._year
&& this->_month == d._month
&& this->_day > d._day) {
return true;
}
else {
return false;
}
}
解读:

日期的判断很简单,我们只需要挨个对比就可以了,

结果返回布尔值,没什么好说的。

为了方便测试,我们再开一个 test.cpp 来放测试用例,方便后续测试我们的代码。

test.cpp

void DateTest1() {
Date d4(2022, 2, 29);
d4.Print();

Date d5(2009, 2, 29);
d5.Print();

Date d6(1998, 2, 29);
d6.Print();
}

void DateTest2() {
Date d1(2022, 2, 1);

Date d2(2012, 5, 1);
cout << (d1 > d2) << endl;

Date d3(2022, 3, 15);
cout << (d1 > d3) << endl;

Date d4(d1);
cout << (d1 > d4) << endl;
}

int main(void)
{
// DateTest1();
DateTest2();

return 0;
}
运行结果如下:

0x02 日期加等天数 operator+=


合并一个日期似乎没什么意义,但是加天数的场景就很多了。

比如我们想让当前日期加100天:

加完后,d1 的日期就是加了100天之后的日期了,我们这里要实现的就是这个功能。

Date.h

class Date {
public:
// ...
Date& operator+=(int day); // d1 += 100

private:
int _year;
int _month;
int _day;
};
日期加天数没有难的,就是一个 "进位" 而已。

把进位搞定就可以了,在动手前我们可以先画个图来分析分析怎么个进法:

很明显,只需要判断加完日期后天数合不合法,

看它加完后的天数有没有超出这个月的天数,如果超过了就不合法。

这个我们刚才已经实现过 GetMonthDay 了,这里就直接拿来用就行了。

如果不合法,我们就进位。天满了往月进,月再满就往年进。

Date.cpp

Date& Date::operator+=(int day) {
this->_day += day; // 把要加的天数倒进d1里
while (this->_day > GetMonthDay(this->_year, this->_month)) { // 判断天数是否需要进位
this->_day -= GetMonthDay(this->_year, this->_month); // 减去当前月的天数
this->_month++; // 月份+1

if (this->_month == 13) { // 判断月份是否需要进位
this->_month = 1; // 重置月份
this->_year++; // 年份+1
}
}

return *this;
}
首先把天数都倒进 d1 里,之后检查一下天数是否溢出了。

如果溢出就进位,这里的逻辑部分通过我们刚才画的图可以很轻松地实现出来,

天满了就往月进位,月满了就往年进位。

最后返回 *this ,把我们加好的 d1 再递交回去就ok了。

因为出了作用域对象还在,我们可以使用引用返回减少拷贝,岂不美哉?

test.c

void DateTest3() {
Date d1(2022, 1, 16);
d1 += 100; // 让当前天数+100天
d1.Print();
}

int main(void)
{
// DateTest1();
// DateTest2();
DateTest3();

return 0;
}
运行结果如下:

0x03 日期加天数 operator+


还是和 += 一样,日期加日期没有什么意义,但是 "日期 + 天数" 还是用得到的。

所以我们重载 operator+ 为一个日期加天数的。

Date.h

class Date {
public:
// ...
Date operator+(int day) const; // d1 + 100

private:
int _year;
int _month;
int _day;
};
+= 是改变 "本体",但是 + 并不会,所以这里可以加个 const 修饰一下。

+ 和 += 很类似,也是通过 "判断" 就可以实现的,

因为我们刚才已经实现过 += 了,所以我们可以做一个巧妙地复用。

复用我们刚才实现的 += ,我们来看看是怎么操作的。

Date.c

/* d1 + 100 */
Date Date::operator+(int day) const {
Date ret(*this); // 拷贝构造一个d1
// ret.operator+=(day);
ret += day; // 巧妙复用+=

return ret; // 出了作用域ret对象就不在了,所以不能用引用返回
}
我们只需要做 "加" 的工作,把算好的日期返回回去就行了,

我们将 "本体" 复制一个 "替身" 来把结果返回去,利用拷贝构造复制出一个 ret 出来,

我们对这个 "替身" 进行加的操作,这样就不会改 "本体" 了

顺便提一下,因为它出了作用域会死翘翘,

所以我们 —— 不能用引用返回!不能用引用返回!不能用引用返回!

重点来了,这里巧妙地复用我们刚才已经实现好的 += ,就可以轻松搞定了:

ret.operator+=(day);
如果觉得看起来不爽,我们甚至可以直接这么写:

ret += day;
就是这么浅显易懂,可读性真的是强到炸。

+= 之后 ret 的值就是加过 day 的值了,并且是赋到 ret 身上的,所以 ——

直接 重仓 return 回去,就大功告成了,我们来测试下代码。

test.cpp

void DateTest4() {
Date d1(2022, 1, 16);
Date ret = d1 + 100;

ret.Print();
}



int main(void)
{
// DateTest1();
// DateTest2();
// DateTest3();
DateTest4();

return 0;
}
运行结果如下:

复用真的是一件很爽的事情,我们下面的讲解还会疯狂地复用的。

0x04 日期减等天数 operator -=
我们刚才实现了 operator+= ,现在我们来实现一下 -= 。

+= 进位,那 -= 自然就是借位。

日期如果不合法,往月去借,月不够了,就往年去借。

Date.h

class Date {
public:
// ...
Date& operator-=(int day); // d1 -= 100

private:
int _year;
int _month;
int _day;
};
我们先把日期减一下,此时如果天数被减成负数了,那我们就需要进行借位操作。

Date.cpp

/* d1 -= 100 */
Date& Date::operator-=(int day) {
this->_day -= day;
while (this->_day <= 0) { // 天数为0或小于0了,就得借位,直到>0为止。
this->_month--; // 向月借
if (this->_month == 0) { // 判断月是否有得借
this->_year--; // 月没得借了,向年借
this->_month = 12; // 年-1了,月份置为12月
}

this->_day += GetMonthDay(this->_year, this->_month); // 把借来的天数加到_day上
}

return *this;
}
如果减完后的天数小于等于 0,就进入循环,向月 "借位" ,

因为已经借出去了,所以把 月份 - 1 。还要考虑月份会不会借完的情况,

月份为 0 的时候就是没得借了,这种情况就向年借,

之后加上通过 GetMonthDay 获取当月对应的天数,就是所谓的 "借",

循环继续判断,直到天数大于0的时候停止,返回 *this 。

出了作用域 *this 还在,所以我们可以使用引用返回 Date& 。

test.cpp

void DateTest5() {
Date d1(2022, 3, 20);
d1 -= 100; // 2021, 12, 10
d1.Print();
}

int main(void)
{
// DateTest1();
// DateTest2();
// DateTest3();
// DateTest4();
DateTest5();

return 0;
}

运行结果如下:

0x05 日期减天数 operator -
一样的,没什么好说的,我们复用一下 -= 就可以把 - 实现出来了。

Date.h

class Date {
public:
// ...
Date operator-(int day) const; // d1 - 100


private:
int _year;
int _month;
int _day;
};
直接复用就完事了,和 operator+ 思路一样。

Date.cpp

/* d1 - 100 */
Date Date::operator-(int day) const {
Date ret(*this); // 拷贝构造一个d1
ret -= day; // ret.operator-=(day);

return ret;
}
为了顺便带大家体验测试代码的重要性,这里的测试部分我们单独拿出来举例。

0x06 体会测试代码的重要性
我们来好好测试一下刚才写的 operator- ,这里我们进行一个详细的测试,

测试减去的 day 跨月,跨年甚至跨闰年的情况,这样哪里出问题我们可以一目了然。

test.cpp

void DateTest6() {
Date d1(2022, 1, 17);

Date ret1 = d1 - 10;
ret1.Print();

Date ret2 = d1 - 17;
ret2.Print();

Date ret3 = d1 - 30;
ret3.Print();

Date ret4 = d1 - 400;
ret4.Print();
}

int main(void)
{
// DateTest1();
// DateTest2();
// DateTest3();
// DateTest4();
// DateTest5();
DateTest6();

return 0;
}
运行结果如下:

Tips:我们在验证的时候可以再网上找这种在线的日期推算器,来验证一下我们写的对不对。

刚才我们正常测试,确实没什么问题,我们来测试个极端的情况,

❓ 如果我们给的是 d1 - -100 呢?

void DateTest6() {
Date d1(2022, 1, 17);

Date ret = d1 - -100;
ret.Print();
}
运行结果如下:

我们这里代码是复用 operator-= 的,所以我们得去看看 operator-=

我们发现,在设计 operator-= 的时候是 <= 0 才算非法的,所以这种情况就没考虑到。

我们可以这么设计,在减天数之前对 day 进行一个特判,

因为你减负的100就相当于加正的100,就变成加了,

⚡ Date.cpp

/* d1 -= 100 */
Date& Date::operator-=(int day) {
if (day < 0) {
return *this += -day;
}

this->_day -= day;
while (this->_day <= 0) { // 天数为0或小于0了,就得借位,直到>0为止。
this->_month--; // 向月借
if (this->_month == 0) { // 判断月是否有得借
this->_year--; // 月没得借了,向年借
this->_month = 12; // 年-1了,月份置为12月
}

this->_day += GetMonthDay(this->_year, this->_month); // 把借来的天数加到_day上
}

return *this;
}


这样就正常了。

我们再把 operator+= 处理一下:

⚡ Date.cpp

/* d1 += 100 */
Date& Date::operator+=(int day) {
if (day < 0) {
return *this -= -day;
}

this->_day += day; // 把要加的天数倒进d1里
while (this->_day > GetMonthDay(this->_year, this->_month)) { // 判断天数是否需要进位
this->_day -= GetMonthDay(this->_year, this->_month); // 减去当前月的天数
this->_month++; // 月份+1

if (this->_month == 13) { // 判断月份是否需要进位
this->_month = 1; // 重置月份
this->_year++; // 年份+1
}
}

return *this;
}
所以多写几个测试用例,来测一测各种情况,是非常有必要的。

0x07 日期后置加加 operator++()
d1++;
++d1;
因为都是 operator++,后置++ 为了跟前置++ 进行区分,增加了 "参数占位" ——

前置++ 就带这个 "占位标记" , 构成函数重载。

operator++(int); // 表示前置++ d1++
operator++(); // 表示后置++ ++d1
我们这里先来实现以下前置++ :

Date.h

Date& operator++(); // ++d1;
因为前置++返回的是加加之后的值,所以我们使用引用返回。

加不加引用就取决于它出了作用域在不在。

Date.cpp

/* ++d1 */
Date& Date::operator++() {
*this += 1;
return *this;
}
这里我们直接复用 +=,加加以后的值就是 *this ,我们返回一下 *this 就行。

0x08 日期前置加加 operator++(int)
"参数占位" ——来充当后置++。

Date.h

Date operator++(int); // d1++;
因为后置++返回的是加加之前的值,所以我们不用引用返回。

Date.cpp

/* d1++ */
Date Date::operator++(int) {
Date ret(*this); // 拷贝构造一个d1
*this += 1;

return ret;
}
我们在加加之前先拷贝构造一个 "替身" 出来,"本体" 加加后,

你可能感兴趣的:(C语言,c++,开发语言)