【C++要笑着学】从零开始实现日期类 | 体会代码的复用 | 提供完整代码(一)

简介: 啊,朋友们好啊。我是柠檬叶子C,上一章我们讲解了运算符重载,本篇将手把手从零开始一步步实现一个Date类,将会对每个步骤进行详细的思考和解读。

写在前面


啊,朋友们好啊。我是柠檬叶子C,上一章我们讲解了运算符重载,本篇将手把手从零开始一步步实现一个Date类,将会对每个步骤进行详细的思考和解读。


Ⅰ.  实现日期类


0x00 引入


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


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


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


0x00 设计构造函数

规范一点,我们声明与定义分离开来。


💬 Date.h


#include <iostream>
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;
}


🚩 运行结果如下:

ca44cff8d7d0e31beb85e058c0a380fd_d6984d68e3814126831298e4c06c41b1.png


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


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


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


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


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


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


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


💬 比如:


Date d3(2022, 13, 15);  // 作为地球人,怎么会有13月呢?
d3.Print();

🚩 运行结果如下:

48b21563e015f62ad9d0b93a42b13a8d_df89adad92ae45bdbfba20e04f49e8c2.png


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


 这合理吗?这不合理!


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


💬 说得好,那这些呢?


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 <iostream>
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 的位置空出来,


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


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

a894a6693d1578a605a191a0e0cd72f8_9788c8f110784c87afaf32e0e1598f00.png

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


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

0ec61003462761d9eea55e22f972220f_ce9c53f3339e4fd48f3e4d42c782a00f.png


💬 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可省略)
  }
}

🚩 运行结果如下:

e23671452bc7bdcfc2f5a60788542d7f_6b5a01393c8b4374af691800122d8e5c.png



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


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


0x01  判断大于 operator>

 比较两个日期的大小。


💬 Date.h


#include <iostream>
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;
}


🚩 运行结果如下:

1115019aa1231caa1120bdeb2e605ff8_d2f98f37492e425197044597b391775a.png


0x02  日期加等天数 operator+=



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

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

e80af920285c971c249918d7248c9e20_295a612279844011adce58471e966945.png

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


💬 Date.h


class Date {
public:
    // ...
    Date& operator+=(int day);      // d1 += 100
private:
  int _year;
  int _month;
  int _day;
};

日期加天数没有难的,就是一个 "进位" 而已。


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

6d5461bb0b8a16ce3d56c2989c3d1ab5_88494682782141d6ab3d378250212368.png


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


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


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


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

15f71c7bbbdebd3eddad6f806c5d4bb6_1fbe0623965c4bdb89d96fece6d79977.png


💬 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;
}

🚩 运行结果如下:

5366eba382ca10ff0bc4878fce005501_28daa07892ed4d228c5166083b656198.png


0x03  日期加天数 operator+


ad40ec1cadb9c930a9f9e2d9ad7af9cd_ba6de25f1f2c4dfd9b8ad4dad9d8a673.png

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

e64e2f3dc30998dd20685c68af6d2b63_da2fbccb8ee94b4da9b4a891b0277a81.png

所以我们重载 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;
}


🚩 运行结果如下:

c04f2c45ebf3f35de5546e5f1ded87ff_aec14b71d2d64a698e991fd19abbf319.png


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


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;
}


🚩 运行结果如下:

5d8dacb34baac27bf9a4476cf4dc3d70_597ec8e095174d449fd283489a4a96e1.png



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;
}

🚩 运行结果如下:

db45916aa1a89a254dad32a96fc34736_34f4d8d6c3ed4c1ca99c0b837ee15cb9.png

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

1f5d7d500ef2fc1ec7a5eadfd1ba27a9_aea6da46be6440ea90dde978e42604f9.png

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


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


void DateTest6() {
  Date d1(2022, 1, 17);
  Date ret = d1 - -100;
  ret.Print();
}

🚩 运行结果如下:

2ad40ceccea80a94bffdb5cbf598d10a_9a4bf27fa3c648679a7b0116ef619011.png



我们这里代码是复用 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;
}

44ff5749c7e6e81f321574bda57f84b6_2e5cb287b3aa461e94f4483700c382ed.png


这样就正常了。


我们再把 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++ ,为了能让编译器直到我们实现的到底是 前置++ 还是 后置++,


这里就到用到一个叫做 "参数占位" 的东西 ——,即:


后置++ 带  " int " ,构成函数重载。


operator++(int);     // 带int,表示后置加加   d1++
operator++();        // 不带, 表示前置加加   ++d1

实现前置++ :


💬 Date.h


Date& operator++();              // ++d1;

因为 前置++ 返回的是加加之后的值,所以我们使用引用返回。


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


💬 Date.cpp


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

这里我们直接复用 +=,加加以后的值就是 *this ,我们返回一下 *this 就行。


实现后置++ :


💬 Date.h


Date operator++(int);          // d1++;

因为后置++返回的是加加之前的值,所以我们不用引用返回。


💬 Date.cpp


/* d1++ */
Date Date::operator++(int) {
  Date ret(*this);   // 为了能返回++之前的值,我们拷贝构造一个d1
  *this += 1;  // 复用+=,让本体+1
  return ret; 
}

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


把替身 ret 返回回去,就实现了返回加加之前的值。


这里要拷贝构造两次,所以我们推荐以后自定义类型++,使用前置++ 。


💬 test.cpp


void DateTest7() {
  Date d1(2022, 3, 20);
  Date ret1 = d1++;   // d1.operator++(&d1, 0);
  Date ret2 = ++d1;   // d1.operator++(&d1);
}
int main(void)
{
  DateTest7();
  retur

🐞 监测结果如下:

6a91cbc16f0d7e00f65dacfbd3312bf1_b5d221517a564e769e806bbe3b63d21f.png



0x08 日期减减 operator--

📚 日期-- 分为 "前置--" 和 "后置--"  


和 operator++ 一样,operator-- 为了能够区分前置和后置,也要用 int 参数占位


后置-- 带  " int " ,构成函数重载。


operator--(int);     // 带int,表示后置减减   d1--
operator--();        // 不带, 表示前置减减   --d1

实现前置-- :


💬 Date.h


Date& operator--();              // --d1

对应的, 前置-- 返回的是减减之后的值,所以我们使用引用返回。


💬 Date.cpp


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

这里我们直接复用 -=,减减以后的值就是 *this ,然后返回 *this 。


实现后置-- :


💬 Date.h


Date operator--(int);          // d1--

因为 后置-- 返回的是减减之前的值,所以我们不用引用返回。


💬 Date.cpp


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

我们在减减之前先拷贝构造一个 "替身" ,待本体加加后,


把替身 ret 返回回去,就实现了返回减减之前的值。


0x09  判断日期是否相同 operator==

💬 Date.h


bool operator==(const Date& d) const;           // d1 == d2

💬 Date.cpp


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

只有年月日都相等才是 true,否则就是 false。

44d6db03cf7b7e77e8986d2bc2f855f3_62b1e84cec394118beda8beea7d8a48c.png

相关文章
|
1天前
|
C++
【C++基础】类class
【C++基础】类class
9 1
|
1天前
|
安全 程序员 编译器
C++程序中的基类与派生类转换
C++程序中的基类与派生类转换
8 1
|
1天前
|
C++
C++程序中的类成员函数
C++程序中的类成员函数
7 1
|
1天前
|
C++
C++程序中的类封装性与信息隐蔽
C++程序中的类封装性与信息隐蔽
8 1
|
1天前
|
C++
C++程序中的类声明与对象定义
C++程序中的类声明与对象定义
9 1
|
1天前
|
数据安全/隐私保护 C++
C++程序中的派生类
C++程序中的派生类
6 1
|
1天前
|
C++
C++程序中的派生类成员访问属性
C++程序中的派生类成员访问属性
8 1
|
1天前
|
编译器 C++
C++程序中的派生类析构函数
C++程序中的派生类析构函数
9 2
|
4天前
|
测试技术 C++
C++|运算符重载(3)|日期类的计算
C++|运算符重载(3)|日期类的计算
|
5天前
|
C语言 C++ 容器
C++ string类
C++ string类
9 0