【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

目录
相关文章
|
3月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
82 0
|
3月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
163 0
|
5月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
160 12
|
6月前
|
编译器 C++
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
|
6月前
|
编译器 C++
类和对象(下)C++
本内容主要讲解C++中的初始化列表、类型转换、静态成员、友元、内部类、匿名对象及对象拷贝时的编译器优化。初始化列表用于成员变量定义初始化,尤其对引用、const及无默认构造函数的类类型变量至关重要。类型转换中,`explicit`可禁用隐式转换。静态成员属类而非对象,受访问限定符约束。内部类是独立类,可增强封装性。匿名对象生命周期短,常用于临时场景。编译器会优化对象拷贝以提高效率。最后,鼓励大家通过重复练习提升技能!
|
7月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
6月前
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
123 16
|
7月前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
6月前
|
存储 编译器 C++
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。
|
6月前
|
安全 C++
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
322 6