【C++修炼之路】4. 类和对象(中):日期类实现(一)

简介: 【C++修炼之路】4. 类和对象(中):日期类实现(一)

C++之类和对象(中)后续


本节目标

1. 日期类的实现

1.0 代码实现(不是最终代码)

1.1 GetMonthDay的实现

1.2 日期类的框架实现

1.3 日期类的运算函数

pass1:= 、==、>=、> 、<、<=的实现

pass2: += 、+天的实现

pass3: -、-=天的实现

pass4:前置++和后置++的实现

pass5:前置--和后置--的实现

pass6:日期减日期

2. 输入流、输出流

2.1 编译链接产生的问题

2.2 解决私有的成员变量问题

2.3 流的总结

3. const成员

3.1 const 限定

3.2 const 修饰权限

3.3 this指针的比较运算符重载

4. 日期(Date)类最终代码:

Date.h

Date.cpp

Test.cpp

5. 取地址及const取地址操作符重载

6. 总结


本节目标


本篇文章衔接类和对象(中),将剩余的部分进行讲解:


  • 1.日期类实现
  • 2.输入流、输出流
  • 3.const成员函数
  • 4.取地址及const取地址操作符重载


1. 日期类的实现


对于日期类来说,其成员变量包括:年(_year)、月(_month)、日(_day)三个成员变量。对于日期类的实现,通常执行的操作是:日期加天数、日期减天数,日期减日期来确定二者之间相差多少时间,但没有日期加日期,因为这个毫无意义。上述提到的日期运算,运算过程中的日期实际上就是日期类创建的对象。


我们知道,每个月的天数都不一定相同,而且还有闰年这个影响因素,因此我们在进行运算的时候需要考虑这些,并且将其封装成一个通过年和月就能确定这个年、月所对应的天数,所以我们可以构造下面的这个函数GetMonthDay。


1.0 代码实现(不是最终代码)

为了便于后续需要,这里先展示代码(目的是展示其中函数的参数类型),不需要对内容进行了解,只需要在讲解时观察参数类型即可

Date.h

#pragma once
#include<iostream>
using namespace std;
class Date
{
public:
  int GetMonthDay(int year, int month)
  {
    static int monthDayArray[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 monthDayArray[month];
    }
  }
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
    // 检查日期是否合法
    if (!(year >= 1
      && (month >= 1 && month <= 12)
      && (day >= 1 && day <= GetMonthDay(year, month))))
    {
      cout << "非法日期" << endl;
    }
  }
  void Print() 
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
  bool operator==(const Date& d);
  // d1 > d2
  bool operator>(const Date& d);
  // d1 >= d2
  bool operator>=(const Date& d);
  bool operator<=(const Date& d);
  bool operator<(const Date& d);
  bool operator!=(const Date& d);
  // d1 += 100
  Date& operator+=(int day);
  // d1 + 100
  Date operator+(int day);
  // d1 -= 100
  Date& operator-=(int day);
  // d1 - 100
  Date operator-(int day);
  // 前置
  Date& operator++();
  // 后置
  Date operator++(int);
  // 前置
  Date& operator--();
  // 后置
  Date operator--(int);
  //日期-日期
  int operator-(const Date& d);
  //int DayCount();
  //void operator<<(ostream& out);
private:
  int _year;
  int _month;
  int _day;
};

Date.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include"Date.h"
bool Date::operator==(const Date& d)
{
  return _year == d._year
    && _month == d._month
    && _day == d._day;
}
// d1 > d2
bool Date::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 Date::operator>=(const Date& d)
{
  return *this > d || *this == d; //这里就复用了前两个函数
}
bool Date::operator<=(const Date& d)
{
  return !(*this > d);//复用
}
bool Date::operator<(const Date& d)
{
  return !(*this >= d);//复用
}
bool Date::operator!=(const Date& d)
{
  return !(*this == d);//复用
}
Date& Date::operator+=(int day)
{
  if (day < 0)
  {
    //return *this -= -day; 和下面的方式均可
    return *this -= abs(day); //复用下面的 -=
  }
  _day += day;
  while (_day > GetMonthDay(_year, _month))
  {
    _day -= GetMonthDay(_year, _month);
    _month += 1;
    if (_month == 13)
    {
      ++_year;
      _month = 1;
    }
  }
  return *this;
}
Date Date::operator+(int day)
{
  Date ret(*this); //*this本身不改变,因此需要再拷贝一个对象,对其进行计算
  ret += day; //复用
  return ret;
}
Date& Date::operator-=(int day)
{
  if (day < 0)
  {
    //return *this -= -day;
    return *this += abs(day);// 复用
  }
  _day -= day;
  while (_day <= 0)
  {
    --_month;
    if (_month == 0)
    {
      --_year;
      _month = 12;
    }
    _day += GetMonthDay(_year, _month);
  }
  return *this;
}
Date Date::operator-(int day)
{
  Date ret(*this);
  ret -= day;//复用
  return ret;
}
//前置++
Date& Date::operator++()
{
  *this += 1;//复用
  return *this;
}
//后置++ 多一个int参数主要是为了和前置区分,构成函数重载
Date Date::operator++(int)
{
  Date tmp(*this);
  *this += 1;//复用
  return tmp;
}
Date& Date::operator--()
{
  *this -= 1;//复用
  return *this;
}
// 后置
Date Date::operator--(int)
{
  Date tmp(*this);
  *this -= 1;//复用
  return tmp;
}
int Date::operator-(const Date& d)
{
  Date max = *this;
  Date min = d;
  int flag = 1;
  if (*this < d)//这里将在下面描述顺序问题
  {
    max = d;
    min = *this;
    flag = -1;
  }
  int n = 0;//计算相差的天数
  while (min != max)
  {
    ++n;
    ++min;
  }
  return n * flag;
}



1.1 GetMonthDay的实现

int GetMonthDay(int year, int month)//获取日期对应的天数
  {
    static int monthDayArray[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 monthDayArray[month];
    }
  }

通过GetMonthday函数我们就可以在求解天数的时候,将对应的年和月传进去,就可以得到对应的天数。(由于后续需要对返回值进行操作,因此不能传引用返回,拷贝返回值是必要的)


但还是可以说明一下,static修饰的变量不会随着函数的结束而销毁,因为其不是存在函数栈帧里面的,而是静态存储的。


微信图片_20230225180236.png

微信图片_20230225180239.png


在这里进行一下conststatic的区分:


对于C/C++来说:


const就是只读的意思,只在声明中使用,意即其所修饰的对象为常量((immutable)),它不能被修改,并存放在常量区。除了被修改之外,他没有别的作用,因此不像static一样,const修饰的变量出了作用域仍然会被销毁。(上面的const修饰引用返回,虽然没警告,但是不代表没错误)

static一般有两个作用,规定作用域和存储方式(静态存储)。对于局部变量,static规定其为静态存储(存放在静态区)方式每次调用的初始值为上一次调用后的值,调用结束后存储空间不释放;对于全局变量,如果以文件划分作用域的话,此变量只在当前文件可见,对于static函数也是如此。static修饰的变量如果没有初始化,则默认为0.


1.2 日期类的框架实现



class Date
{
public:
  int GetMonthDay(int year, int month)//获取日期对应的天数
  {
    static int monthDayArray[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 monthDayArray[month];
    }
  }
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
    //检查日期是否合法,公元前除外
    if (!(year >= 1
      && (month >= 1 && month <= 12)
      && (day >= 1 && day <= GetMonthDay(year, month))))
    {
      cout << "非法日期" << endl;
    }
  }
  void Print()
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};

抛开运算之外,我们一个整体的日期类的框架就搭建好了,需要注意的是在自定义的构造函数中,我们给其缺省值并在内部判断日期是否合法,像除了闰年的年份不存在2月29号,类似于这样的非法日期一旦传入,其就会出现非法日期的提示。


日期类的运算函数


对于运算函数,如果都将全部内容放到类中,这样会使类的内容看起来很多,因此我们这样操作:对于常用的或者较短的函数我们可以放在类中当成内联,对于这些运算的函数采取声明和定义分离的形式展示(分离后,函数的定义采用类::限定符来确定是属于这个类中的函数),并且我们将这些运算能进行一定的复用从而减少代码量


框架搭建好之后,就需要利用我们上个文章中的运算符重载的内容将日期的加、减、等操作进行一系列的实现:


pass1:= 、==、>=、> 、<、<=的实现

上篇文章中,我们在运算符重载已经展示了这几种运算、这里直接展示:


bool Date::operator==(const Date& d2)
  {
    return _year == d2._year
      && _month == d2._month
      && _day == d2._day;
  }
bool Date::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;
  }
  // d1 >= d2
bool Date::operator>=(const Date& d)//*this是d1,传的d就是d2的别名
  {
    return *this > d || *this == d;
  }
  //…… <<= !=
bool Date::operator<=(const Date& d)
{
  return !(*this > d);//复用
}
bool Date::operator<(const Date& d)
{
  return !(*this >= d);//复用
}
1
2


pass2: += 、+天的实现

(一般是加天数、日期加日期没有意义)

那么接下来展示+=和+这些运算符的计算:我们直接演示正确的方式:


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;
}
Date Date::operator+(int day)
{
    Date ret(*this);
    ret += day;//这里也是运算符重载,复用了+=
    return ret;
}


首先,对于+=,是改变了自身的值,因此我们需要对其本身进行操作,最后返回的时候由于出了作用域*this(上篇解释过为什么不会销毁)还在,并不会被销毁,因此在这里可以加上引用返回避免了拷贝的过程。


其次,对于+,不能改变自身的值,因此,我们需要重新拷贝一下this指针对应的对象,对拷贝后的对象进行操作,这样才不会改变原本的对象,。


将这两个运算符重载放入类之后那我们看看操作前和操作后值的变化:


操作前:

微信图片_20230221221840.png

操作后:

微信图片_20230221221845.png


这样就完成了+和+=的要求,当然,上篇提到过链式加,在这里同样是可以的,因为返回类型是Date

对于加天来说,事实上,不用运算符重载也是可以的,即,像这样的函数也是可以计算的:


Date AddDay(int day)//3.这样也可以加天,但是可读性不如operator
{}
Date JiaTian(int day)//这是啥,运算符重载的意义是可读性。
{}


但实际上,我们采用运算符重载,就是让代码的可读性更高,让其可以像普通内置类型一样的加减,而不是调用函数,调用函数也就是失去了运算符重载的意义。


pass3: -、-=天的实现


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


pass4:前置++和后置++的实现


对于此类,我们在运算符重载时不能区分,都是operator++()。因此,C++规定:将括号中带有int的规定为后置++,不带int的为前置++ 。(int后面可以加参数,也可以不加)


//前置++
Date& Date::operator++()
{
  *this += 1;//复用
  return *this;
}
//后置++ 多一个int参数主要是为了和前置区分,构成函数重载
Date Date::operator++(int)
{
  Date tmp(*this);
  *this += 1;//复用
  return tmp;
}

微信图片_20230225180505.png

从这个可以看出,自定义类型的后置++的效率没有前置++的效率高,因为多了拷贝的过程。


pass5:前置–和后置–的实现


依旧是和++相同的规定:将括号中带有int的规定为后置–,不带int的为前置–

Date& Date::operator--()
{
  *this -= 1;//复用
  return *this;
}
// 后置
Date Date::operator--(int)
{
  Date tmp(*this);
  *this -= 1;//复用
  return tmp;
}


微信图片_20230225180556.png


pass6:日期减日期


对于日期减日期,实际上就是两个对象相减的过程,在减之前,我们需要判断这两个对象的大小关系,然后用大日期减去小日期,小日期减去大日期的结果为负,但我们相减的目的是为了看相距的时间,因此小日期减去大日期没有意义。


日期间日期有很多的方法,最麻烦的就是逐年逐月逐日的去减,因为需要考虑对应的天数或者是不是闰年,因此这种方式是不可取的。此外呢,对于自己的思考来说,我想到一种求解的方法:假如我们以1年1月1日也就是公元第一天为基准,建立一个通过日期求天数的函数,当把这两个日期都求出来具体天数之后,再进行相减,就可以解决。此外,仍有一种方式,就是先通过运算符重载的比较函数比较日期大小,再拷贝小的日期为一个新的对象,判断这个对象与大的是否相等,不相等就++这个对象(运算符重载)


方法1:直接根据基准量求天数函数,再相减


那我们来看看代码:


int Date::DayCount()//求天数的函数
{
  Date tmp(*this);
  int day = 0;
  while (tmp._year > 0)
  {
    while (tmp._month > 0)
    {
      while (tmp._day > 0)
      {
        day += tmp._day;
        tmp._day = 0;
      }
            //要注意的是不能先--月,否则对应不上
      tmp._day = GetMonthDay(tmp._year, tmp._month--);
    }
    tmp._year--;
    tmp._month = 12;
  }
  return day;
}
//日期-日期
int Date::operator-(Date& d)
{
  int day = d.DayCount();
  int Day = DayCount();//实际上是this->DayCount() , this省略了
  return Day - day;
}


微信图片_20230225180717.png

从这里看出,我们上面的函数是没问题的。


方法2:小日期拷贝++比较大日期


代码:

int Date::operator-(const Date& d) 
{
  Date max = *this;
  Date min = d;
  int flag = 1;
  if (*this < d)//这里将在下面描述顺序问题
  {
    max = d;
    min = *this;
    flag = -1;
  }
  int n = 0;//计算相差的天数
  while (min != max)
  {
    ++n;
    ++min;
  }
  return n*flag;
}


这样,同样是可以的。并且比上面的代码简洁并且思路简单,下面就以方法二的代码一一描述。


对于上面的日期类的实现,实际上我们已经将所有的功能完成,只需要进行整理即可成为标准的日期类,但是,我们在程序中发现,*this总是出现在左侧,因为我们知道,对于比较*this和d来说,也属于运算符重载,即*this < d可以看成(*this).operator<(d),那既然我们认为*this和d是同一个类型,那我们是否可以将其调换位置,从*this < d改成d > *this呢,即变成d.operator>(*this)呢?


微信图片_20230225180801.png

我们发现,这是不行的,当然,我们知道operator左右有所区别,但是具体的区别是什么呢?后面将会进行详细的讲解。

此外,由于日期类整体的代码还可以进一步更新,因此整体代码将会在下面展示。(3.3)


2. 输入流、输出流


在这之前,我们知道对于流提取(cin)和流插入(cout)都是库中的函数,并且其能识别类型进行输入输出,那么为什么他能够识别任意类型呢?实际上,对于这两个函数,在库里面以函数重载的方式存在,因此,其才能够识别任意类型。


微信图片_20230225180851.png

当然,任意类型所说的是内置类型,对于自定义的类类型,事实上其没有符合的重载函数,因此也是没办法进行识别的,因此想要以<<运算符识别类,就需要自己来写运算符重载函数。


微信图片_20230225180917.png


对于流来说,我们有输入流和输出流,也就是IO流,我们可以在C++的头文件<iostream>这个库函数清晰的看到,那么对于流提取和流插入来说,其分别属于ostreamistream


说了这么多,那我们展示一下具体的步骤:(仍然以Date类为基础)


微信图片_20230225180958.png

然而我们清晰的发现,这样定义是错误的,因为运算符重载展开之后是不符合逻辑的

微信图片_20230225181001.png

修改成这样之后,就可以编译成功。


但是这样也就是失去了我们原本的初衷,本来是cout << d1,而现在却变成了d1<<cout,本来是人骑马,现在却变成了马骑人,虽然符合逻辑,但是却不符合伦理。造成这种现象的原因实际是隐藏的this指针不能修改位置,即this指针一定在运算符重载的左侧,因此也就造成了d1会在左侧。因此,为了避免这样的情况,我们需要将其修改成不用this指针的形式,进而推出,我们需要将这个运算符重载函数从类里面拿出来,进行单独定义。


微信图片_20230225181054.png


当我们单拿出来之后,会发现,这样访问的变量是私有变量,由于这个函数不在类中,我们通过这个函数不能访问私有变量,因此也就是需要把private的限定解开,变成public,虽然这样可以,但却失去了对于类内部成员的保护。此外,还有第二个问题,需要我们把私有的变成公有才能进行演示。


/

2.1编译链接产生的问题


继续接着上面讨论


微信图片_20230225181132.png

我们发现,这样变成公有之后仍然运行不成功,实际上还有着编译链接的问题存在:由于我们在Date.h中存在全局的operator<<()函数,因此在编译链接的过程中,其分别会在Date.cpp和Test.cpp重复展开,这就导致了在生成符号表时自己与自己产生冲突,因此运行失败。实际上,此问题对于所有的函数都会存在,因此这也是我们需要避免的。将其变成内联不生成符号表


第一种方式我们知道,因此主要讲述后两种方式。先来看一下第二种方式的运行结果:

微信图片_20230225181150.png

即这样就可以以人骑马的方式正确的运行。对于static修饰的函数来说,由于是静态的,因此并不会在别的的文件产生符号表,只会在Date.h中产生,这也就使得不会在Date.cpp产生符号表,因此也就不会产生冲突。


还要说明的一点是,由于cout也存在链式调用(在类和对象中描述过)因此,我们需要把void变成ostream&


因此我们把private变成公有,先讨论第二个解决的方法,事实上有三种方法可以解决


声明和定义分离(声明不占用符号表)

将Date.h的全局函数变成静态的,即static修饰函数

在Date.h中变成内联(编译时自动展开,不生成符号表)


第一种方式我们知道,因此主要讲述后两种方式。先来看一下第二种方式的运行结果:


微信图片_20230225181245.png


微信图片_20230225181249.png

  • 这里做一个补充:对于头文件中的pragma once是对头文件中的重复声明进行去重的,而不是对所有文件去重。

对于内联函数,直接在Date.h中同样不会产生问题

微信图片_20230225181252.png



因此总结一下,static和inline修饰都不会产生符号表,但是原因不同,static是静态只在Date.h作用;inline是直接展开,不看做函数。


以上就是对编译链接这部分问题的分析过程。


2.2 解决私有的成员变量问题


最后,别忘了,这些都是基于私有变成公有的结果,我们真正需要的是用私有去解决这个问题,即成员变量必须是私有的。而2.1中的问题前提都是将私有变成公有之后才方便演示的。因此,在这里将解决私有成员变量的问题。


  • 方法:在类的任意位置对该函数进行友元声明


微信图片_20230225181703.png

通过在类的内部的任意位置通过friend进行友元声明,这样就相当于给此全局函数开了绿灯,即可以访问私有成员变量(直接偷家)。


微信图片_20230225181709.png

因此,这样才是解决此问题的最终方案。需要注意的是友元函数不易声明过多,因为这样会造成耦合度过高导致程序不易观察。


2.3 流的总结


上面的一共讨论了两个问题并得到了合理的解决。

  1. 编译链接的问题:利用static或者inline进行处理。
  2. 私有成员变量访问的问题:通过友元函数声明解决。


此外,上面我们详细解释了流提取的操作,相应的,还有流插入,流插入与流提取很类似,不同的地方就在于流提取是istream并且符号是>>,同样对流提取也进行inline和友元声明。同样的可以返回引用,因为cin也是全局的函数并不会被销毁。


微信图片_20230225181809.png

微信图片_20230225181813.png


即:通过自定义流的重载,就可以将我们自定义的类进行输入和输出,Print方法就可以忽略了。

相关文章
|
17天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
20 4
|
17天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
18 4
|
17天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
17 1
|
18天前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
15 0
|
23天前
|
编译器 C++ 数据库管理
C++之类与对象(完结撒花篇)(下)
C++之类与对象(完结撒花篇)(下)
28 0
|
27天前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
27天前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
29天前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
23 3
|
29天前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
51 1
|
29天前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
18 1