【C++】类和对象练习——日期类的实现(一)

简介: 【C++】类和对象练习——日期类的实现

前言

在上一篇文章我们学习类和对象的过程中,我们不是写了一个日期类嘛。

但是我们之前实现的日期类并不是很完整,我们只是借助它来帮大家学习类和对象的知识。

那这篇文章呢,我们就在之前的基础上,再增添一些功能,实现一个比较完整的日期类,作为一个练习,来帮助我们更好的理解我们之前学过的知识。

这是我们之前一起写的不太完整的日期类:

class Date
{
public:
  //构造函数
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  //拷贝构造函数
  Date(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
  bool operator<(const Date& d)
  {
    if (_year < d._year)
      return true;
    else if (_year == d._year && _month < d._month)
      return true;
    else if (_year == d._year && _month == d._month && _day < d._day)
      return true;
    return false;
  }
  bool operator<=(const Date& d)
  {
    return *this == d || *this < d;
  }
  bool operator>(const Date& d)
  {
    return !(*this <= d);
  }
  bool operator>=(const Date& d)
  {
    return !(*this < d);
  }
  bool operator!=(const Date& d)
  {
    return !(*this == d);
  }
  void Print()
  {
    cout << _year << "-" << _month << "-" << _day << endl;
  }
  bool operator==(const Date& d)
  {
    return _year == d._year
      && _month == d._month
      && _day == d._day;
  }
  //赋值重载(对于Date类用默认生成的就行)
  //d1=d2(this就是d1,d就是d2)
  /*Date& operator=(const Date& d)
  {
    if (this != &d)
    {
      _year = d._year;
      _month = d._month;
      _day = d._day;
    }
    return *this;
  }*/
private:
  int _year;
  int _month;
  int _day;
};

那这些实现过的函数我们就不再一一讲解了,大家不熟悉的话可以回顾一下上一篇文章。

另外呢,我们最终实现的是一个完整的日期类,那方便对代码进行维护和管理,以及对实现好的日期类进行测试,我们还是像之前写数据结构一样,放在多个文件中。

1. 日期的合法性判断

我们之前呢已经给该日期类写好了构造函数:

Date(int year = 1, int month = 1, int day = 1)
{
  _year = year;
  _month = month;
  _day = day;
}

并且还指定了缺省参数,那这样的话在实例化一个对象时我们就可以自己给对象指定初值,我们输入的日期是啥,该对象就会被初始化为对应的日期。

那现在有一个问题,如果我们实例化对象时给的日期不合法呢?比如:

void Datetest1()
{
  Date d1(2023, 2, 30);
  d1.Print();
}

4d8477144a5d43d6a6a2df37e4052e08.png

不合法是不是也判断不出来啊。

那我们就对原来的构造函数做一些补充好吧,让它在给对象初始化的时候可以去判断一下对应的日期合不合法。

那要怎么判断呢?

给我们一个年月日,要判断是否合法,是不是要判断月在不在【1,12】之内以及天数有没有超过当前月的总天数啊。
但是某个月的天数是不是不好确定啊,不同月的天数不一样,而且要考虑平闰年。

那我们先来写一个获取某年某月天数的函数:

int Date::GetMonthDay(int year, int month)
{
  assert(month > 0 && month < 13);
  int MonthArr[13] = { 0,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 MonthArr[month];
  }
}

不过多解释了,相信大家都能看懂。

那有了这个函数,我们就可以在构造函数中去判断了:

Date::Date(int year, int month, int day)
{
  if (month > 0 && month < 13 
    && day>0 && day <= GetMonthDay(year, month))
  {
    _year = year;
    _month = month;
    _day = day;
  }
  else
  {
    cout << "日期非法" << endl;
    _year = 1;
    _month = 1;
    _day = 1;
  }
}

如果合法了,我们再去赋值,不合法就还全搞成1,要不然就是随机值了。

那这时我们再用非法日期去初始化对象:

59abf2aa7acd40f08bb44dbb9f1b852b.png

这样输入的日期不合法就提示了。

2. 日期+天数(+/+=)

我们说日期+日期是不是没啥意义啊,但是日期+一个天数是不是还有点意义,可以理解成这么多天之后是几几年几月几日。

2.1 +和+=的重载

所以接下来,我们要实现一个功能就是计算一个日期加了一个天数之后得到的日期:


那具体实现的思路呢可以这样搞:

首先我们想让自定义类型Date的对象直接和整型相加,这里肯定要对+进行运算符重载。

我们拿到一个日期一个天数之后,先把天数加到日上,然后,判断此时的日是否超过了当前月的总天数(获取月份天数的函数我们之前已经实现过了),超过的话,就减去当前月的天数,然后月份++,那月份++有可能会超过12啊,一旦超过,就让年++,然后月置成1。

那当然这肯定是一个循环,当我们的日(day)不再大于当前月的天数,则此时得到的就是最终的日期。

我们来写一下代码:

Date& Date::operator+(int day)
{
  _day += day;
  while (_day > GetMonthDay(_year, _month))
  {
    _day -= GetMonthDay(_year, _month);
    _month++;
    if (_month == 13)
    {
      _year++;
      _month = 1;
    }
  }
  return *this;
}

这里*this出了作用域还在,所以可以返回引用。

那我们来测试一下:

ad082a5c31e246d683fcf1c49b68db40.png

b680b4cdbe134d0598b7ee2306600eda.png

我们看这个计算出来的日期确实是没问题的,但是d1+100,这里是+而不是+=,所以d1是不是不应该变啊,我们只是把d1+100得到的日期赋给了d2,但是现在d1也变了。

所以我们这样写其实实现的是啥,是不是+=啊。

所以我们这里重载的应该是+=:

Date& Date::operator+=(int day)
{
  _day += day;
  while (_day > GetMonthDay(_year, _month))
  {
    _day -= GetMonthDay(_year, _month);
    _month++;
    if (_month == 13)
    {
      _year++;
      _month = 1;
    }
  }
  return *this;
}

那+=还需要有返回值吗,🆗,最好还是带返回值,带返回值的话就可以支持这种情况:d2 = d1 += 100;

那刚才实现的是+=,那+要这么搞?

是不是借助一个临时对象就可以搞定了啊,我们只要不改变*this就行了嘛。

Date Date::operator+(int day)
{
  Date tmp(*this);
  tmp._day += day;
  while (tmp._day > GetMonthDay(tmp._year, tmp._month))
  {
    tmp._day -= GetMonthDay(tmp._year, tmp._month);
    tmp._month++;
    if (tmp._month == 13)
    {
      tmp._year++;
      tmp._month = 1;
    }
  }
  return tmp;
}

那就这样,测试一下:

52e7b0a01de44824bd47f71bcfba7f11.png

🆗,这次d1没有被改变。

acca532be7f748f09504d172868d1476.png

答案也没问题。


那对于+的重载:


大家有没有注意到我们没有返回引用,为什么?

因为我们返回的是啥,是不是tmp,而tmp是一个局部的对象,出了作用域就销毁了,所以我们不能返回引用。

那有时候呢,有的人为了这个地方能够返回引用,对tmp加了一个static修饰,然后就返回引用。

129a41641222402ebd2a0f3f8e6d5dac.png

大家思考一下这样可以吗?

我们试一下会发现:

84f9ca24ebad4e2db6bd6b8b9f1448fb.png

第一次没问题,但我们第二次在调用+,是不是就错了啊。

7fafb6a1f7b14114bf54e97c45b88a92.png

第二次我们还是给d1+1000天,但结果却是2000天之后的。


为什么会这样?


原因就在于我们用了static。

我们创建的局部对象tmp被static修饰后,就不存在栈区了,而是放在静态区了,所以静态局部变量出作用域不会被销毁,而是保留在静态区,因此我们确实可以返回引用了。

但是,静态局部变量的初值是在编译期间就指定的,所以运行期间,不管我们再去调用几次函数,tmp都不会被重新初始化,而是保留上次的值,所以我们第二次的结果才会变成2000天之后。

之前有篇文章详解static的作用,大家可以看——链接: link

所以呢,这个地方我们不要为了能够返回引用而去随便加static。

2.2 对于两者复用的讨论

那除此之外:

大家看,+的重载与+=的重载,除了多一个临时的局部对象,其它的逻辑是不是一样啊,所以+里面是不是可以复用+=啊。

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

这样是不是就行了啊:

0e32d2532b2147358618569594a2e8e8.png

那既然+可以复用+=,我们是不是也可以考虑先实现+,然后+=复用+呢?

当然可以。

Date Date::operator+(int day)
{
  Date tmp(*this);
  tmp._day += day;
  while (tmp._day > GetMonthDay(tmp._year, tmp._month))
  {
    tmp._day -= GetMonthDay(tmp._year, tmp._month);
    tmp._month++;
    if (tmp._month == 13)
    {
      tmp._year++;
      tmp._month = 1;
    }
  }
  return tmp;
}

那这是我们自己写的+,现在我们+=来复用+。

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

这样不就行了。


那再来思考:


到底是+复用+=好呢,还是+=复用+好呢?

🆗,那这里呢,其实是我们的第一种即+复用+=更好一点。

因为+里面创建临时对象有一次拷贝,返回的是值而不是引用又是一次拷贝。

那如果是+复用+=的话就只有+里有拷贝,但如果是+=复用+的话,是不是+=和+里都有拷贝了。

3. 前置++和后置++重载

刚重载了+和+=,那是不是还有前置++和后置++啊,那我们也来实现一下。

先来前置++吧:

来回忆一下前置++的特性是什么?
是不是先++,后使用啊,即返回的是++之后的值。

举个例子:

int a=5;
int b=++a;

首先不管前置++,还是后置++,a的值肯定都会+1,那现在++a前置,先++后使用,返回++之后的值,所以b也是6。

那我们来重载一下:

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

很简单,直接复用+=。

那我们再来重载后置++:

后置++呢,是先使用,后++,即返回++之前的值。那我们还是可以借助一个临时对象。

但是呢?

这里会发现有一个问题:

b1ff33d0adb949b2bb4f9694a337ac04.png

前置++和后置++没法区分啊,它们的参数和函数名是不是一样啊。

欸,那这怎么解决啊?

🆗,那当然是有办法的。

前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载。C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递(它的作用就是为了构成重载),编译器自动传递。

34220110089a42d28cda34d6b9aac086.png

所以呢,这样搞就行了。

我们来实现一下:

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

🆗,我们就搞定了。

那实现完了,大家看一下:

前置++和后置++那个效率会高一点。

是不是前置啊,因为与后置相比,前置没有拷贝啊对不对。

所以,对于内置类型来说,大家可以认为前置后置没有区别;但是对于自定义类型来说,能用前置,尽量用前置++。

我们的前置++和后置++就搞定了,那在调用的地方呢,我们正常去用就行了,编译器会自动识别匹配。
遇到前置++,它就会自动去调用前置++;遇到后置++,编译器会自动传一个整型参数,去调用后置++。

4. 日期-天数(-/-=)

上面我们通过重载+进而实现计算一个日期+一个天数之后是什么日期,那是不是还可以搞一下日期-天数,计算它的前多少天是什么日期。

那我们先来重载-=:

那思路和上面加一个天数也是类似的,我们先用日减去传过来的天数,如果减完之后<=0,说明当前日期是不合法的,那怎么办,就去加上一个月的天数,然后月–,当然要注意判断会不会减到上一年,然后还<=0的话,就继续,知道日>0循环结束。

Date& Date::operator-=(int day)
{
  _day -= day;
  while (_day <= 0)
  {
    --_month;
    if (_month == 0)
    {
      --_year;
      _month = 12;
    }
    _day += GetMonthDay(_year, _month);
  }
  return *this;
}

测试一下:

7cf65f9c753f4476a087330fae902859.png

66a05a6cf96a47268d80a2338754fe65.png

没问题。

然后我们再重载一下-:

那是不是简单啊,直接复用-=。

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

测试 一下:

7552bddf92d248f8a8c5920fb078cd18.png

🆗,看起来好像没什么问题了,但是,如果这样呢:

a74448cf5b0c43fb82c4798b2e2ea051.png

这里减一个负数,会发现结果就错了。

为什么呢,大家可以调试观察:bf0fc11bccb74766b0afa8d8f79afa1a.png

会发现这里根本不会进入循环,直接就返回了。

所以针对负数的情况,我们要单独处理一下:

Date& Date::operator-=(int day)
{
  if (day < 0)
  {
    *this += -day;
    return *this;
  }
  _day -= day;
  while (_day <= 0)
  {
    --_month;
    if (_month == 0)
    {
      --_year;
      _month = 12;
    }
    _day += GetMonthDay(_year, _month);
  }
  return *this;
}

我们-是复用-=的,所以这里在-=里面处理。

-=一个-100就相当于+=一个100嘛。43246968847347d0ab45bac62cf6fc59.png

那同样对于+和+=我们也要处理一下:

8ad6024beac74af6a0794b07f9c99031.png

82a0bd7b51b8428586974f2e84aef9a2.png

那我们来处理一下:

Date& Date::operator+=(int day)
{
  if (day < 0)
  {
    *this -= -day;
    return *this;
  }
  _day += day;
  while (_day > GetMonthDay(_year, _month))
  {
    _day -= GetMonthDay(_year, _month);
    _month++;
    if (_month == 13)
    {
      _year++;
      _month = 1;
    }
  }
  return *this;
}

这样是不是就行了,+=一个-100就相当于-=一个100嘛。

2be5b37841254313889b8f7f78029784.png

目录
相关文章
|
6天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
28 5
|
12天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
40 4
|
13天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
37 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
22 1
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1
|
1月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
20 1