【C++初阶(六)】类和对象(中)与日期类的实现

简介: 【C++初阶(六)】类和对象(中)与日期类的实现


类的6个默认成员函数

如果一个类中什么成员都没有,我们简称其为空类。但是空类中真的什么都没有吗?其实不然,任何一个类,即使我们什么都不写,类中也会自动生成6个默认成员函数。

class Date {}; //空类

注意:这里的“默认”和“缺省”的意思差不多,也就是你不写这6个函数,编译器会自动生成,你若是写了,则编译器就不生成了。

构造函数

构造函数的概念

构造函数:名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。

例如,以下日期类中的成员函数Date就是一个构造函数。当你用该日期类创建一个对象时,编译器会自动调用该构造函数对新创建的变量进行初始化。

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)// 构造函数
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "年" << _month << "月" << _day << "日" << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};

构造函数的特性

一、构造函数的函数名与类名相同

二、构造函数无返回值

 这里所说的构造函数无返回值是真的无返回值,而不是说返回值为void。

三、对象实例化时编译器自动调用对应的构造函数

 当你用类创建一个对象时,编译器会自动调用该类的构造函数对新创建的变量进行初始化。

四、构造函数支持重载

 这意味着你可以有多种初始化对象的方式,编译器会根据你所传递的参数去调用对应的构造函数。

一般来说,构造函数一般写全缺省要好一点。

五、无参的构造函数、全缺省的构造函数以及我们不写编译器自动生成的构造函数都称为默认构造函数,并且默认构造函数只能有一个

 初学C++时,你可能认为只有当我们不写,编译器自动生成的构造函数才被称为默认构造函数。其实并不是这样的,以下3种都叫做默认构造函数:

 1、我们不写,编译器自动生成的构造函数。

 2、我们自己写的无参的构造函数。

 3、我们自己写的全缺省的构造函数。

总而言之,无需传参就可以调用的构造函数就是默认构造函数。

六、如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,若用户显示定义了,则编译器就不再生成

 说到这里,你可能会想:既然在我们不写的情况下,编译器会自动生成一个构造函数,那我们就没有必要自己写构造函数了。这种想法是不对的。

#include <iostream>
using namespace std;
class Date
{
public:
  void Print()
  {
    cout << _year << "年" << _month << "月" << _day << "日" << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1; // 编译器将调用自动生成的默认构造函数对d1进行初始化
  d1.Print();
  return 0;
}

嗯…最终d1当中的年月日都是随机值。

 到这里你可能会产生疑问:d1对象调用了编译器自动生成的构造函数后,d1对象的_year/_month/_day依旧是随机值,那这编译器自动生成的构造函数还有什么意义?

这就要说明一下编译器自动生成的构造函数机制

构造函数,也是默认成员函数,我们不写,编译器会自动生成

编译生成的默认构造的特点:

1、我们不写才会生成,我们写了任意一个构造函数就不会生成了

2、内置类型的成员不会处理(祖师爷后悔了,在C++11,声明支持给缺省值,就相当于补内置类型的坑)

3、自定义类型的成员才会处理,回去调用这个成员的默认构造函数

在数据结初阶我们实现了Myqueue函数,因为要用到栈的知识。我们先实现一下栈类。

class Stack
{
public:
  Stack(size_t n = 4)
  {
    cout << "Stack(size_t n = 4)" << endl;
    if (n == 0)
    {
      a = nullptr;
      top = capacity = 0;
    }
    else
    {
      a = (int*)malloc(sizeof(int) * n);
      if (a == nullptr)
      {
        perror("realloc fail");
        exit(-1);
      }
      top = 0;
      capacity = n;
    }
  }
  ~Stack()
  {
    cout << "~Stack()" << endl;
    free(a);
    a = nullptr;
    top = capacity = 0;
  }
  void Push(int x)
  {
    if (top == capacity)
    {
      size_t newcapacity = capacity == 0 ? 4 : capacity * 2;
      int* tmp = (int*)realloc(a, sizeof(int) * newcapacity);
      if (tmp == nullptr)
      {
        perror("realloc fail");
        exit(-1);
      }
      if (tmp == a)
      {
        cout << capacity << "原地扩容" << endl;
      }
      else
      {
        cout << capacity << "异地扩容" << endl;
      }
      a = tmp;
      capacity = newcapacity;
    }
    a[top++] = x;
  }
  int Top()
  {
    return a[top - 1];
  }
  void Pop()
  {
    assert(top > 0);
    --top;
  }
  void Destroy()
  {
    free(a);
    a = nullptr;
    top = capacity = 0;
  }
  bool Empty()
  {
    return top == 0;
  }
private:
  // 成员变量
  int* a;
  int top;
  int capacity;
};

Myqueue:

// 两个栈实现一个队列
class MyQueue
{
private:
  Stack _pushst;
  Stack _popst;
};

Myqueue函数会调用这个成员的默认构造函数:

总结一下:虽然在我们不写的情况下,编译器会自动生成构造函数,但是编译器自动生成的构造函数可能达不到我们想要的效果,所以大多数情况下都需要我们自己写构造函数。

析构函数

析构函数概念:

与构造函数功能相反,析构函数负责完成对象的销毁,对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。

我们知道当一个类对象销毁时,其中的局部变量也会随着该对象的销毁而销毁,例如,我们用日期类创建了一个对象d1,当d1被销毁时,对象d1当中的局部变量_year/_month/_day也会被编译器销毁。

 但是这并不意味着析构函数没有什么意义。像栈(Stack)这样的类对象,当该对象被销毁时,其中动态开辟的栈并不会随之被销毁,需要我们对其进行空间释放,这时析构函数的意义就体现了。

析构函数的特性

一、析构函数的函数名是在类名前加上字符‘~

class Date
{
public:
  Date()// 构造函数
  {}
  ~Date()// 析构函数
  {}
private:
  int _year;
  int _month;
  int _day;
};

二、析构函数无参数,无返回值

 析构函数所谓的无返回值也是真的无返回值,而不是返回值为void。

三、对象生命周期结束时,C++编译器会自动调用析构函数

 这就大大降低了C语言中栈空间忘记释放问题的发生,因为当栈对象生命周期结束时,C++编译器会自动调用析构函数对其栈空间进行释放。

四、一个类有且只有一个析构函数。若未显示定义系统会自动生成默认的析构函数

编译器自动生成的析构函数机制:

 1、编译器自动生成的析构函数对内置类型不做处理。

 2、对于自定义类型,编译器会再去调用它们自己的默认析构函数。

五、先构造的后析构,后构造的先析构

 因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合先进后出的原则。

拷贝构造

拷贝构造函数的概念

拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用从const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

#include <iostream>
using namespace std;
class Date
{
public:
  Date(int year = 0, 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;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2023, 11, 12);
  Date d2(d1); // 用已存在的对象d1创建对象d2
  return 0;
}

拷贝构造函数的特性

一、拷贝构造函数是构造函数的一个重载形式

 因为拷贝构造函数的函数名也与类名相同。

 推荐加一个const,加入写失误,写反了,也不会出错。(权限的缩小)

二、拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用

要调用拷贝构造函数就需要先传参,若传参使用传值传参,那么在传参过程中又需要进行对象的拷贝构造,如此循环往复,最终引发无穷递归调用。

**小贴士:**自定义类型的对象进行函数传参时,一般推荐使用引用传参。使用传值传参也可以,但每次传参时都会调用拷贝构造函数。

三、若未显示定义拷贝构造函数,系统将生成默认的拷贝构造函数

看看以下代码:

#include <iostream>
using namespace std;
class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)// 构造函数
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "年" << _month << "月" << _day << "日" << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1(2023, 11, 12);
  Date d2(d1); // 用已存在的对象d1创建对象d2
  d1.Print();
  d2.Print();
  return 0;
}

代码中,我们自己并没有定义拷贝构造函数,但编译器自动生成的拷贝构造函数最终还是完成了对象的拷贝构造。

编译器自动生成的拷贝构造函数机制:

 1、编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝(值拷贝)。

 2、对于自定义类型,编译器会再去调用它们自己的默认拷贝构造函数。

四、编译器自动生成的拷贝构造函数不能实现深拷贝

 上面说到,编译器自动生成的拷贝构造函数会对内置类型完成浅拷贝。对于以下这句代码,浅拷贝实际上就是将d1的内容完完全全的复制了一份拷贝给d2,所以说浅拷贝也叫做值拷贝。

Date d2(d1);// 用已存在的对象d1创建对象d2

但某些场景下浅拷贝并不能达到我们想要的效果。例如,栈(Stack)这样的类,编译器自动生成的拷贝构造函数就不能满足我们的需求了:

Stack s1;
Stack s2(s1);// 用已存在的对象s1创建对象s2

代码中,我们的本意是用已存在的对象s1创建对象s2,但编译器自动生成的拷贝构造函数,完成的是浅拷贝,拷贝出来的对象s2将不能满足我们的要求。

举个例子,现有以下栈(Stack)类:

class Stack
{
public:
  Stack(int capacity = 4)
  {
    _ps = (int*)malloc(sizeof(int)* capacity);
    _size = 0;
    _capacity = capacity;
  }
  void Print()
  {
    cout << _ps << endl;// 打印栈空间地址
  }
private:
  int* _ps;
  int _size;
  int _capacity;
};

我们可以看到,类中没有自己定义拷贝构造函数,那么当我们用已存在的对象来创建另一个对象时,将调用编译器自动生成的拷贝构造函数。看看以下代码运行结果:

int main()
{
  Stack s1;
  s1.Print();// 打印s1栈空间的地址
  Stack s2(s1);// 用已存在的对象s1创建对象s2
  s2.Print();// 打印s2栈空间的地址
  return 0;
}

结果打印s1栈和s2栈空间的地址相同,这就意味着,就算在创建完s2栈后,我们对s1栈做的任何操作都会直接影响到s2栈。

这是我们想要的效果吗?显然不是,我们希望在创建时,s2栈和s1栈中的数据是相同的,但是在创建完s2栈后,我们对s1栈和s2栈之间的任何操作能够互不影响。

 而且这种情况下,还会出现对同一块空间释放多次的问题。若我们自己定义的析构函数是正确的情况下,当程序运行结束,s2栈将被析构,此时那块栈空间被释放,然后s1栈也要被析构,再次对那一块空间进行释放。

 可以看到,这种情况下编译器自动生成的拷贝构造函数就不能满足我们的要求了。

在这右不得不提起我们的Myqueue了,对于默认生成的默认函数非常受用,简直人生赢家:

总结一下:

 1、像Date这样的类,需要的就是浅拷贝,那么编译器自动生成的拷贝构造函数就够用了,我们不需要自己写。

 2、像Stack这样的类,浅拷贝会导致析构两次、程序崩溃等问题,需要我们自己写对应的拷贝构造函数。

赋值运算符重载

运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,其目的就是让自定义类型可以像内置类型一样可以直接使用运算符进行操作

d1 == d2;// 可读性高(书写简单)
IsSame(d1, d2);// 可读性差(书写麻烦)

运算符重载函数也具有自己的返回值类型,函数名字以及参数列表。其返回值类型和参数列表与普通函数类似。

运算符重载函数名为:关键字operator后面接需要重载的操作符符号。
函数原型:返回值 operator运算符(参数列表)

注意:

 1.不能通过连接其他符号来创建新的操作符:比如operator@。

 2.重载操作符必须有一个类类型或枚举类型的操作数。

 3.用于内置类型的操作符,重载后其含义不能改变。

 4.作为类成员的重载函数时,函数有一个默认的形参this,限定为第一个形参。

 5.sizeof 、:: 、.* 、?: 、. 这5个运算符不能重载。

这里以重载 == 运算符作为例子:

我们可以将该运算符重载函数作为类的一个成员函数,此时该函数的第一个形参默认为this指针。

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "年" << _month << "月" << _day << "日" << endl;
  }
  bool operator==(const Date& d)// 运算符重载函数
  {
    return _year == d._year
      &&_month == d._month
      &&_day == d._day;
  }
private:
  int _year;
  int _month;
  int _day;
};

我们也可以将该运算符重载函数放在类外面,但此时外部无法访问类中的成员变量,这时我们可以将类中的成员变量设置为共有(public),这样外部就可以访问该类的成员变量了(也可以用友元函数解决该问题)。并且在类外没有this指针,所以此时函数的形参我们必须显示的设置两个。

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "年" << _month << "月" << _day << "日" << endl;
  }
  int _year;
  int _month;
  int _day;
};
bool operator==(const Date& d1, const Date& d2)// 运算符重载函数
{
  return d1._year == d2._year
    &&d1._month == d2._month
    &&d1._day == d2._day;
}

赋值运算符重载

这里以重载 = 运算符作为例子:

class Date
{
public:
  Date(int year = 0, int month = 1, int day = 1)// 构造函数
  {
    _year = year;
    _month = month;
    _day = day;
  }
  Date& operator=(const Date& d)// 赋值运算符重载函数
  {
    if (this != &d)
    {
      _year = d._year;
      _month = d._month;
      _day = d._day;
    }
    return *this;
  }
  void Print()// 打印函数
  {
    cout << _year << "年" << _month << "月" << _day << "日" << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};

重载赋值运算符需要注意以下几点:

一、参数类型设置为引用,并用const进行修饰

 赋值运算符重载函数的第一个形参默认是this指针,第二个形参是我们赋值运算符的右操作数。

 由于是自定义类型传参,我们若是使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用引用传参(第一个参数是默认的this指针,我们管不了)。

 其次,第二个参数,即赋值运算符的右操作数,我们在函数体内不会对其进行修改,所以最好加上const进行修饰。

二、函数的返回值使用引用返回

 实际上,我们若是只以d2 = d1这种方式使用赋值运算符,赋值运算符重载函数就没必要有返回值,因为在函数体内已经通过this指针对d2进行了修改。但是为了支持连续赋值,即d3 = d2 = d1,我们就需要为函数设置一个返回值了,而且很明显,返回值应该是赋值运算符的左操作数,即this指针指向的对象。

 和使用引用传参的道理一样,为了避免不必要的拷贝,我们最好还是使用引用返回,因为此时出了函数作用域this指针指向的对象并没有被销毁,所以可以使用引用返回。

三、赋值前检查是否是给自己赋值

 若是出现d1 = d1,我们不必进行赋值操作,因为自己赋值给自己是没有必要进行的。所以在进行赋值操作前可以先判断是否是给自己赋值,避免不必要的赋值操作。

四、引用返回的是*this

 赋值操作进行完毕时,我们应该返回赋值运算符的左操作数,而在函数体内我们只能通过this指针访问到左操作数,所以要返回左操作数就只能返回*this。

五、一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝

 没错,赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝,例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去,类似于memcpy。

 对于日期类,编译器自动生成的赋值运算符重载函数就可以满足我们的需求,我们可以不用自己写。但是这也不意味着所有的类都不用我们自己写赋值运算符重载函数,当遇到一些特殊的类,我们还是得自己动手写赋值运算符函数的。

注意区别以下代码所调用的函数:

Date d1(2023, 11, 12);
  Date d2(d1);
  Date d3 = d1;

这里一个三句代码,我们现在都知道第二句代码调用的是拷贝构造函数,那么第三句代码呢?调用的是哪一个函数?是赋值运算符重载函数吗?

 其实第三句代码调用的也是拷贝构造函数,注意区分拷贝构造函数和赋值运算符重载函数的使用场景:

拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
 赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。

const成员

const修饰类的成员函数

 我们将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。

例如,我们可以对类成员函数中的打印函数进行const修饰,避免在函数体内不小心修改了对象:

void Print()const// cosnt修饰的打印函数
  {
    cout << _year << "年" << _month << "月" << _day << "日" << endl;
  }

思考下面几个问题(经典面试题):

1.const对象可以调用非const成员函数吗?

2.非const对象可以调用const成员函数吗?

3.const成员函数内可以调用其他的非const成员函数吗?

4.非cosnt成员函数内可以调用其他的cosnt成员函数吗?

答案是:不可以、可以、不可以、可以

解释如下:


 1.非const成员函数,即成员函数的this指针没有被const所修饰,我们传入一个被const修饰的对象,用没有被const修饰的this指针进行接收,属于权限的放大,函数调用失败。


 2.const成员函数,即成员函数的this指针被const所修饰,我们传入一个没有被const修饰的对象,用被const修饰的this指针进行接收,属于权限的缩小,函数调用成功。


 3.在一个被const所修饰的成员函数中调用其他没有被const所修饰的成员函数,也就是将一个被const修饰的this指针的值赋值给一个没有被const修饰的this指针,属于权限的放大,函数调用失败。


 4.在一个没有被const所修饰的成员函数中调用其他被const所修饰的成员函数,也就是将一个没有被const修饰的this指针的值赋值给一个被const修饰的this指针,属于权限的缩小,函数调用成功。

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

取地址操作符重载和const取地址操作符重载,这两个默认成员函数一般不用自己重新定义,使用编译器自动生成的就行了:

class Date
{
public:
  Date* operator&()// 取地址操作符重载
  {
    return this;
  }
  const Date* operator&()const// const取地址操作符重载
  {
    return this;
  }
private:
  int _year;
  int _month;
  int _day;
};

日期类的实现:

在学习了C++的6个默认成员函数后,我们现在动手实现一个完整的日期类,来加强对这6个默认成员函数的认识。

我们实现声明定义分离:

这是日期类中所包含的成员函数和成员变量:(Data.h)

class Date
{
     // 友元声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
  // 构造函数
  Date(int year = 0, int month = 1, int day = 1);
  // 打印函数
  void Print() const;
    //赋值重载
    Date& operator=(const Date& d);
  // 日期+=天数
  Date& operator+=(int day);
  // 日期+天数
  Date operator+(int day) const;
  // 日期-=天数
  Date& operator-=(int day);
  // 日期-天数
  Date operator-(int day) const;
  // 函数重载
     // 运算符重载
      // ++d1 -> d1.operator++()
  // 前置++
  Date& operator++();
  // d1++ -> d1.operator++(0)
   // 加一个int参数,进行占位,跟前置++构成函数重载进行区分
   // 本质后置++调用,编译器进行特殊处理
  // 后置++
  Date operator++(int);
  // 前置--
  Date& operator--();
  // 后置--
  Date operator--(int);
  // 日期的大小关系比较
  // 总结一下:只读函数可以加const,内部不涉及修改成员的都是只读函数
  bool operator>(const Date& d) const;
  bool operator>=(const Date& d) const;
  bool operator<(const Date& d) const;
  bool operator<=(const Date& d) const;
  bool operator==(const Date& d) const;
  bool operator!=(const Date& d) const;
  // 日期-日期
  int operator-(const Date& d) const;
  // 析构,拷贝构造,赋值重载可以不写,使用默认生成的即可
private:
  int _year;
  int _month;
  int _day;
};

打印函数:

// 打印函数
void Date::Print() const
{
  cout << _year << "年" << _month << "月" << _day << "日" << endl;
}

构造函数

进入构造函数体,首先需要检查日期的合法性,只有当日期合法时,才能进行后续的构造操作。

// 获取某年某月的天数
inline int GetMonthDay(int year, int month)
{
  // 数组存储平年每个月的天数
  static int dayArray[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  int day = dayArray[month];
  if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
  {
    //闰年2月的天数
    day = 29;
  }
  return day;
}
// 构造函数
Date::Date(int year, int month, int day)
{
  _year = year;
    _month = month;
   _day = day;
// 检查日期是否合法
if (month < 1 || month > 12
  || day < 1 || day > GetMonthDay(year, month))
{
  cout << "非法日期" << endl;
  // exit(-1);
}
  }
}

GetMonthDay函数中的三个细节:

 1.该函数可能被多次调用,所以我们最好将其设置为内联函数。

 2.函数中存储每月天数的数组最好是用static修饰,存储在静态区,避免每次调用该函数都需要重新开辟数组。

 3.逻辑与应该先判断month == 2是否为真,因为当不是2月的时候我们不必判断是不是闰年。

注意:当函数声明和定义分开时,在声明时注明缺省参数,定义时不标出缺省参数。

void TestDate1()
{
  Date d1(2023, 7, 23);
  Date d2;
  d1.Print();
  d2.Print();
  Date d3(2010, 2, 29);
  d3.Print();
  Date d4(2023, 13, 29);
  d4.Print();
}

检查一下:

赋值重载=

Date& Date::operator=(const Date& d)
{
  if (this != &d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
  return *this;
}

测试一下:

//测试赋值重载
void TestDate2()
{
  Date d1(2023, 11, 14);
  // 拷贝构造,一个已经存在的对象去初始化另一个要创建对象
  Date d2(d1);
  d1.Print();
  d2.Print();
  Date d3(2023, 11, 15);
  // 赋值,两个已经存在对象进行拷贝
  //d1 = d3;  // d1.operator=(d3)
  d1 = d2 = d3;
  d1.Print();
  d2.Print();
  d3.Print();
  d1 = d1;
  int i, j;
  i = j = 10;
  cout << (j = 10) << endl;
}

测试结果:

日期 += 天数

对于+=运算符,我们先将需要加的天数加到日上面,然后判断日期是否合法,若不合法,则通过不断调整,直到日期合法为止。

调整日期的思路:

 1.若日已满,则日减去当前月的天数,月加一。

 2.若月已满,则将年加一,月置为1。

反复执行1和2,直到日期合法为止。

// 日期+=天数
Date& Date::operator+=(int day)
{
  if (day<0)
  {
    // 复用operator-=
    *this -= -day;
  }
  else
  {
    _day += day;
    // 日期不合法,通过不断调整,直到最后日期合法为止
    while (_day > GetMonthDay(_year, _month))
    {
      _day -= GetMonthDay(_year, _month);
      _month++;
      if (_month > 12)
      {
        _year++;
        _month = 1;
      }
    }
  }
  return *this;
}

注:当需要加的天数为负数时,转而调用-=运算符重载函数。

测试+=

//测试+=
void TestDate3()
{
  Date d1(2023, 11, 14);
  d1 += 200;
  d1.Print(); 
}

测试结果:

200天后是2024年6月1日,那么对不对呢?

我们用日期计算器计算一下:

发现没有问题。

日期 + 天数

+运算符的重载,我们可以复用上面已经实现的+=运算符的重载函数。但是要注意:虽然我们返回的是加了之后的值,但是对象本身的值并没有改变。就像a = b + 1,b + 1的返回值是b + 1,但是b的值并没有改变。所以我们还可以用const对该函数进行修饰,防止函数内部改变了this指针指向的对象。

// 日期+天数
Date Date::operator+(int day) const
{
  Date tmp(*this);// 拷贝构造tmp,用于返回
  // 复用operator+=
  tmp += day;
  return tmp;
}

注意:+=运算符的重载函数采用的是引用返回,因为出了函数作用域,this指针指向的对象没有被销毁。但+运算符的重载函数的返回值只能是传值返回,因为出了函数作用域,对象tmp就被销毁了,不能使用引用返回。

日期 -= 天数

对于-=运算符,我们先用日减去需要减的天数,然后判断日期是否合法,若不合法,则通过不断调整,直到日期合法为止。

调整日期的思路:

 1.若日为负数,则月减一。

 2.若月为0,则年减一,月置为12。

 3.日加上当前月的天数。

反复执行1、2和3,直到日期合法为止。

// 日期-=天数
Date& Date::operator-=(int day)
{
  if (day < 0)
  {
    // 复用operator+=
    *this += -day;
  }
  else
  {
    _day -= day;
    // 日期不合法,通过不断调整,直到最后日期合法为止
    while (_day <= 0)
    {
      _month--;
      if (_month == 0)
      {
        _year--;
        _month = 12;
      }
      _day += GetMonthDay(_year, _month);
    }
  }
  return *this;
}

注:当需要减的天数为负数时,转而调用+=运算符重载函数。

日期 - 天数

和+运算符的重载类似,我们可以复用上面已经实现的-=运算符的重载函数,而且最好用const对该函数进行修饰,防止函数内部改变了this指针指向的对象。

// 日期-天数
Date Date::operator-(int day) const
{
  Date tmp(*this);// 拷贝构造tmp,用于返回
  // 复用operator-=
  tmp -= day;
  return tmp;
}

注意:-=运算符的重载函数采用的是引用返回,但-运算符的重载函数的返回值只能是传值返回,也是由于-运算符重载函数中的tmp对象出了函数作用域被销毁了,所以不能使用引用返回。

测试-=

//测试-=
void TestDate4()
{
  Date d1(2023, 11, 14);
  d1 -= 200;
  d1.Print();
  Date d2(2023, 11, 14);
  d2 += -200;
  d2.Print();
  Date d3(2023, 11, 14);
  d3 -= -200;
  d3.Print();
}

测试结果:

前置 ++

前置++,我们可以复用+=运算符的重载函数。

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

后置 ++

由于前置++和后置++的运算符均为++,为了区分它们的运算符重载,我们给后置++的运算符重载的参数加上一个int型参数,使用后置++时不需要给这个int参数传入实参,因为这里int参数的作用只是为了跟前置++构成重载。

// 后置++
Date Date::operator++(int)
{
  Date tmp(*this);// 拷贝构造tmp,用于返回
  // 复用operator+=
  *this += 1;
  return tmp;
}

注意:后置++也是需要返回加了之前的值,只能先用对象tmp保存之前的值,然后再然对象加一,最后返回tmp对象。由于tmp对象出了该函数作用域就被销毁了,所以后置++只能使用传值返回,而前置++可以使用引用返回。

测试一下:

void TestDate5()
{
  Date d1(2023, 11, 14);
  Date ret1 = d1++;
  //Date ret1 = d1.operator++(0);
  ret1.Print();
  d1.Print();
  Date ret2 = ++d1;
  //Date ret2 = d1.operator++();
  ret2.Print();
  d1.Print();
}

测试结果:

前置 –

前置–,我们也是可以复用前面的-=运算符的重载函数。

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

后置–

后置–需要注意的事项和后置++是一样的,我这里就不过多阐述了。

// 后置--
Date Date::operator--(int)
{
  Date tmp(*this);// 拷贝构造tmp,用于返回
  // 复用operator-=
  *this -= 1;
  return tmp;
}

日期类的大小关系比较

日期类的大小关系比较需要重载的运算符看起来有6个,实际上我们只用实现两个就可以了,然后其他的通过复用这两个就可以实现。

注意:进行日期的大小比较,我们并不会改变传入对象的值,所以这6个运算符重载函数都应该被const所修饰。

>运算符的重载

>运算符的重载很简单,先判断年是否大于,再判断月是否大于,最后判断日是否大于,这其中有一者为真则函数返回true,否则返回false。

bool Date::operator>(const Date& d) const
{
  if (_year > d._year)
  {
    return true;
  }
  else if (_year == d._year)
  {
    if (_month > d._month)
    {
      return true;
    }
    else if (_month == d._month)
    {
      if (_day > d._day)
      {
        return true;
      }
    }
  }
  return false;
}

==运算符的重载

==运算符的重载也是很简单,年月日均相等,则为真。

bool Date::operator==(const Date& d) const
{
  return _year == d._year
    &&_month == d._month
    &&_day == d._day;
}

>=运算符的重载

>=,即大于或者等于,满足其中之一即可。
bool Date::operator>=(const Date& d) const
{
  return *this > d || *this == d;
}

<运算符的重载

<,大于等于的反面即是小于。

bool Date::operator<(const Date& d) const
{
  return !(*this >= d);
}

<=运算符的重载

<=,大于的返回即是小于等于。

bool Date::operator<=(const Date& d) const
{
  return !(*this > d);
}

!=运算符的重载

!=,等于的反面即是不等于。

bool Date::operator!=(const Date& d) const
{
  return !(*this == d);
}

日期 - 日期

日期 - 日期,即计算传入的两个日期相差的天数。我们只需要让较小的日期的天数一直加一,直到最后和较大的日期相等即可,这个过程中较小日期所加的总天数便是这两个日期之间差值的绝对值。若是第一个日期大于第二个日期,则返回这个差值的正值,若第一个日期小于第二个日期,则返回这个差值的负值。

// 日期-日期
int Date::operator-(const Date& d) const
{
  Date max = *this;// 假设第一个日期较大
  Date min = d;// 假设第二个日期较小
  int flag = 1;// 此时结果应该为正值
  if (*this < d)
  {
    // 假设错误,更正
    max = d;
    min = *this;
    flag = -1;// 此时结果应该为负值
  }
  int n = 0;// 记录所加的总天数
  while (min != max)
  {
    min++;// 较小的日期++
    n++;// 总天数++
  }
  return n*flag;
}

测试一下:

代码中使用flag变量标记返回值的正负,flag为1代表返回的是正值,flag为-1代表返回的是负值,最后返回总天数与flag相乘之后的值即可。

输出

ostream& operator<<(ostream& out, const Date& d)
{
  out << d._year << "/" << d._month << "/" << d._day << endl;
  return out;
}

输入

istream& operator>>(istream& in, Date& d)
{
  in >> d._year >> d._month >> d._day;
  return in;
}


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