天天看点

类和对象·日期类的实现

类和对象·日期类的实现
你好,我是安然无虞。

文章目录

  • ​​自学网站​​
  • ​​写在前面​​
  • ​​日期类的实现​​
  • ​​cin和cout重载问题​​
  • ​​const成员函数​​
  • ​​大厂面试真题​​

自学网站

推荐给老铁们两款学习网站:

面试利器&算法学习:​​​牛客网​​​ 风趣幽默的学人工智能:​​人工智能学习​​ ​

写在前面

上一节我们学习了构造函数、析构函数、拷贝构造和赋值重载,今天我们就要好好运用前面学习的知识来完成日期类的实现。

日期类的实现

下面先把Date类基本的成员补充完整:

class Date
{
public:
  //全缺省的构造函数
  Date(int year = 2022, int month = 9, int day = 7)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  //拷贝构造
  Date(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
  //析构函数
  ~Date()
  {
    _year = 0;
    _month = 0;
    _day = 0;
  }
  
  //赋值重载
  //对于赋值重载函数需要注意的是连续赋值的情况
  //至于返回值是Date还是Date&由返回对象在函数结束后是否销毁决定
  //这里因为返回的是*this,所以可以使用引用
  //d1 = d2 = d3
  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;
}      

OK,是不是很简单,下面也不难哦。

// ==运算符重载
bool operator==(const Date& d)
{
  return (_year == d._year && _month == d._month && _day == d._day);
}

// >运算符重载
bool operator>(const Date& d)
{
  if((_year > d._year 
  || (_year == d._year && _month > d._month)
  || (_year == d._year && _month == d._month && _day > d._day))
    return true;
  else
    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) || (*this == d);
}      

上面这部分代码,主要是想让大家体会代码的复用性。

我们知道还有++,-- 这类的运算符,它们还分为前置和后置,因为函数名相同,那么该如何区分呢?

// 如若要区分前置和后置,需构成重载
// 规定:如果是后置,需要多加一个参数,这个形参传什么都可以,仅为了区分
// 如:Date operator++();//前置
//    Date opeartor++(int);//后置
// 前置++
Date& operator++()
{
  *this += 1;
  return *this;
}

// 后置++
Date operator++(int)//形参写成int i也可以,无所谓
{
  Date ret(*this);
  *this += 1;
  return ret;
}

// 这里需要格外注意:为什么前置++返回值是Date&,而后置++返回值是Date?
// 我们知道返回值为Date,会调用拷贝构造,这样会影响效率,而Date&却不用。
// 那为什么后置++不用引用返回呢?
// 因为后置++是先赋值再++,返回的是++前的值,所以首先拷贝构造一个ret,返回的是ret的值
// 因为ret是局部对象,所以不能传引用返回。      

好,既然前置++和后置++已经给出了,那么前置 --和后置-- 就很容易啦。

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

// 后置--
Date operator--(int)
{
  Date ret(*this);
  *this -= 1;
  return ret;
}      

好的,上面的内容都很简单,下面我们稍微加大一点难度哦。

为了实现日期±天数,首先实现这两个函数:

// 判断闰年
bool isLeapYear(int year)
{
  return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)); 
}

//获取每个月的天数
int GetMonthDay(int year, int month)
{
  int MonthDay[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  if(isLeapYear(year))
    MonthDay[2] = 29;
  return MonthDay[month];
}

// 因为GetMonthDay函数可能会被频繁调用,每次都开数组影响效率,所以这部分代码还可以优化一下:
static int MonthDay[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};//静态成员只会初始化一次
if(isLeapYear(year))
    MonthDay[2] = 29;
return MonthDay[month];
//注意哦,这里我给大家挖了一个坑,上面的写法有一个bug,你发现了吗?

//MonthDay[2] = 29;会修改数组里的数据,导致MonthDay[2]变成29。你可以调试去感受。

//综上,下面的代码才是最完美的。
int GetMonthDay(int year, int month)
{
  const static int MonthDay[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
  if(isLeapYear(year) && month == 2)
    return 29;
  else
    return MonthDay[month];
}      
// 日期+=天数
// d1 += 2;
Date& operator+=(int day)
{
  //注意day是负数时的情况
  if (day < 0)
    return *this -= -day;

  _day += day;
  while (_day > GetMonthDay(_year, _month))
  {
    _day -= GetMonthDay(_year, _month);
    ++_month;
    if (_month == 13)
    {
      ++_year;
      _month = 1;
    }
  }

  return *this;
}

// 日期+天数
// d1 + 2
Date operator+(int day)
{
  //Date ret(*this);

  //ret._day += day;
  //while (ret._day > GetMonthDay(ret._year, ret._month))
  //{
  //  ret._day -= GetMonthDay(ret._year, ret._month);
  //  ret._month++;
  //  if (ret._month == 13)
  //  {
  //    ++ret._year;
  //    ret._month = 1;
  //  }
  //}

  //return ret;
  
  //可以用复用+=
  Date ret(*this);
  ret += day;
  return ret;
}      

这里又有一个问题了:+复用+=,+=复用+,哪个更优呢?

首先我们知道实现+运算符本身就要调用两次拷贝构造,而实现+=本身没有拷贝构造。如果是+=复用+的话,有4次拷贝构造(+=是用+实现的嘛,所以调用+=本身就有两次拷贝构造,然而实现部分的+又要调用两次拷贝构造,拢共4次);如果是+复用+=的话,只有2次拷贝构造。(日期-=天数同理)

所以说,以后我们只需要实现+=,-=,其他的直接复用它们即可。

// 日期-=天数
// d1 -= 2;
Date& operator-=(int day)
{
  if (day < 0)
    return *this += -day;

  _day -= day;
  while (_day <= 0)
  {
    --_month;
    if (_month == 0)
    {
      _month = 12;
      --_year;
    }

    _day += GetMonthDay(_year, _month);
  }

  return *this;
}

// 日期-天数
// d1 - 2
Date operator-(int day)
{
  Date ret = *this;
  ret -= day;
  return ret;
}      

在计算日期±天数的时候,大家先在草稿纸上面计算哦,想好了再写代码。OK,下面讲解最后一部分,日期-日期,下面给出的方法挺妙的哦,不信你看看:

// 日期-日期
int operator-(const Date& d)
{
  //假设较大值是*this
  Date max = *this;
  Date min = d;
  int flag = 1;
  int count = 0;
  
  if(max < min)
  {
    max = d;
    min = *this;
    flag = -1;
  }
  while(min != max)
  {
    min++;
    count++;
  }
  return flag * count;
}      

cin和cout重载问题

哈哈,你不会真的以为没有了吧,怎么会呢,还有补充部分。

对日期类的补充:

上面我们已经对日期类的大部分功能实现完毕了,但是如果直接执行这段代码还是存在问题:

void TestDate()
{
  Date d1;
  //对于自定义类型的对象流提取操作符>>和流插入操作符<<需要自己实现
  cin >> d1;// >>是流提取操作符
  cout << d1;// <<是流插入操作符
}      

注意哦:

cin 是istream类型的对象,cout是ostream类型的对象,它们都是iostream头文件里面全局属性的对象。

类和对象·日期类的实现
int i = 1;
double d = 2.2;
cout << i << d << endl;      

从上面的代码中我们可以看到,相比于C语言,不需要%d,%lf这些指定的输入输出格式,这是为什么呢?其实又是函数重载,C++库里面已经给了内置类型的<<,>>也一样。

类和对象·日期类的实现

所以这里说明了什么问题呢,说明了自定义类型的流插入和流提取需要我们自己实现。

如果我们在类中实现:

class Date
{
public:
  ...
  //成员函数里的第一个形参是隐藏的this指针
  void operator<<(ostream& out)
  {
    out << _year << "-" << _month << "-" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};      

如果像上面这样,将operator<<重载到类中实现,那么调用的时候会变成下面这样:

Date d1(2022, 9, 29);
d1.operator<<(cout);// 即d1 << cout;
// 这样是不是很别扭,但是没办法,成员函数的第一个参数默认是隐藏的this指针      

因为将<<运算符重载成成员函数后,第一个参数默认是隐藏的this指针,所以我们调用的时候会很怪,这个时候需要将cout 调成第一个参数才可以,也就是说不能把<<重载成成员函数,那我们该怎么办呢?

可以实现成全局函数:

void operator<<(ostream& out, const Date& d)
{
  // 如果重载成全局函数的话,还有一个问题就是不能访问类中的私有成员
  out << d._year << " " << d._month << " " << d._day << endl;//报错
}

class Date
{
public:
  ...
private:
  int _year;
  int _month;
  int _day;
};      

解决将<<运算符重载成全局函数,不能访问类中私有成员的问题,有两种解决方法:

方法一:

这种方法Java语言喜欢使用,就是在类中定义GetYear(), SetYear()…这些public成员函数,以便在类外进行读写类中私有成员,但是这样实现的话,跟将私有成员变成公有成员的差距不大了,因为这个时候既能读私有成员,又能写成员变量,影响封装,所以不好,C++不推荐这种写法;

方法二:

提供友元,就是在类中声明这个全局变量是该类的友元函数(我是你的朋友,我就可以访问你的私有了),具体关于友元的细节将在下一节进行讲解。

void operator<<(ostream& out, const Date& d)
{
  // 如果重载成全局函数的话,还有一个问题就是不能访问类中的私有成员
  out << d._year << " " << d._month << " " << d._day << endl;//报错
}

class Date
{
  //友元函数
  void operator<<(ostream& out, const Date& d);
public:
  ...
private:
  int _year;
  int _month;
  int _day;
};      

代码像上面这样实现之后,就可以正常使用了:

Date d1(2022, 9, 29);
cout << d1;      

但是还存在一个问题就是:

cout << d1 << d2;      

这样就编译不通过了,原因是没有给<<运算符重载提供返回值,导致其不支持连续赋值,所以我们需要给函数提供一个返回值,以便再去做下一次流插入的左操作数,具体实现如下:

//流插入操作符:<<
ostream& operator<<(ostream& out, const Date& d)
{
  out << d._year << " " << d._month << " " << d._day << endl;
  return out;
}      

这样实现之后,<<就可以正常使用啦,当然了,流提取操作符>>原理同上:

//流提取操作符:>>
istream& operator>>(istream& in, Date& d)//注意哦,此时不能加上const,因为d需要修改
{
  in >> d._year >> d._month >> d._day;
  return in;
}      

const成员函数

我们将const修饰类的成员函数叫做const成员函数,const修饰类成员函数,实际上是修饰该成员函数隐藏的this指针,表明在该函数中不能对类的任何成员进行修改操作。

//Date.h文件
class Date
{
public:
  //...
  void Print()
  {
    cout << _year << " " << _month << " " << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};      
//test.cpp文件
#include"Date.h"
void Func(const Date& d)
{
  d.Print();
}

int main()
{
  Date d1(2022,9,29);
  d1.Print();
  Func(d1);
  return 0;
}      

如果我们直接这样编译的话是编译不通过的,错误显示如下:

类和对象·日期类的实现

咦,这个报错是什么鬼?

我们知道执行这一行代码的时候,编译器是这样处理的:

d1.Print(); //d1.Print(&d1);
//由于d1是Date类型,所以&d1是Date*      

这样的话,Print()成员函数第一个隐藏的this指针类型也就是这样的:

//Date* this,由于this的指向不能发生改变,
//所以编译器会将成员函数的第一个隐藏的this指针处理成Date* const this;
void Print()
{
  cout << _year << " " << _month << " " << _day << endl;
}      

可能有老铁会说:对呀,这个你说的,我都理解呀,那为什么会编译不通过呢?

其实呀,如果仅执行 d1.Print();是不会报错的,错就错在 Func(d1); 这个函数体里面调用的Print()。为什么这样说呢,请继续看:

#include"Date.h"
void Func(const Date& d)
{
  d.Print();//d.Print(&d);
  //这里的&d类型是const Date*
}

//而我们知道成员函数Print()第一个隐藏的this指针类型是Date* 
//将const Date* 传递给 Date* const(本质还是Date*)属于权限放大,这个是不允许的,所以编译报错
//只允许权限不变或者权限缩小
//d1.Print();//即d1.Print(&d1);Date*传递给Date*属于权限不变
void Print()
{
  cout << _year << " " << _month << " " << _day << endl;
}      

那么我们该如何修改呢?我们知道如果要解决Func()函数里面的错误的话,这需要将成员函数Print()里面的第一个隐藏的this指针类型改成const Date* const即可,但是由于它是隐藏的,不能显示修改,那我们该怎么办呢?不要担心,很简单,像下面这样即可:

//在函数后面加上const,相当于在第一个参数(也就是隐藏的this指针)的前面加上const
//这个时候this指针的类型就变成了const Date* const类型
void Print() const
{
  cout << _year << " " << _month << " " << _day << endl;
}      

在成员函数的后面加上const之后,this类型就变成了const Date* const类型,属于权限很低的类型,这样的好处是不管传递过来的变量类型是Date*(权限缩小),还是const Date*(权限不变)编译都能通过,不会存在权限放大这么一说了。

可能有老铁会问了,既然给成员函数后面加上const之后可以有效避免权限放大的问题,那么我们为什么不在每个成员函数的后面都加上一个呢?

我要说的是,加上const之后,this指针变成const Date* const类型,所以此时this指向不能修改,this指向的内容同样也不能修改,所以说只有成员变量不用修改的成员函数才可以加上const。比如日期类里面的++, --不可以加,而+, -可以。

好的,有了前面的基础,请看下面几个问题:

  1. const对象可以调用非const成员函数吗?
  2. 非const对象可以调用const成员函数吗?
  3. const成员函数内可以调用其他的非const成员函数吗?
  4. 非const成员函数内可以调用其他的const成员函数吗?

这就很简单啦,问题1不可以,权限放大;问题2可以,权限缩小;问题3不可以,权限放大;问题4可以,权限缩小。

大厂面试真题

1.已知表达式++a中的"++"是作为成员函数重载的运算符,则与++a等效的运算符函数调用形式为( )

A.a.operator++()

B.a.operator++(0)

C.a.operator++(int)

D.operator++(a,0)

解析:这道题很简单,前置++嘛

2.在重载一个运算符为成员函数时,其参数表中没有任何参数,这说明该运算符是( )

A.无操作数的运算符

B.二元运算符

C.前缀一元运算符

D.后缀一元运算符

解析:重载为成员函数时,其函数的参数个数与真实的函数参数个数会减少1个,减少的则通过this指针进行传递,所以无参则说明有一个参数,然后对比前置后置++,就很容易解决本题啦。

3.哪个操作符不能被重载 ( )

A.*

B.()

C… (点)

D.[]

E.->

解析:不能被重载的运算符只有5个, 点号. 三目运算?: 作用域访 问符:: 运算符sizeof 以及.*

继续阅读