【与C++的邂逅】--- 类和对象(中)

 Welcome to 9ilk's Code World

       

(๑•́ ₃ •̀๑) 个人主页:        9ilk

(๑•́ ₃ •̀๑) 文章专栏:     与C++的邂逅 


本篇博客我们将学习类和对象中,认识类的六个默认成员函数以及实现日期类。下图为本节思维导图。


🏠 类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6 个默认成员函数。
class Date {}; //空类

注:这里的“默认”与之前讲的“缺省”意思类似,意思是用户没有显示实现,编译器会自动生成。

🏠 构造函数

📌 构造函数概念

对于Date类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置
信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?如果有时忘记先调用Init初始化,这时会导致日期是随机值,可能会导致其他麻烦,这时C++提供了构造函数这个解决方案。
class Date
{
public:
 void Init(int year, int month, int day)
 { 
  _year = year;
  _month = month;
  _day = day;
 }

 void Print()
 {
   cout << _year << "-" << _month << "-" << _day << endl;
 }

private:
 int _year;
 int _month;
 int _day;
};

int main()
{
  Date d1;
  d1.Init(2022, 7, 5);
  d1.Print();
  Date d2;
  d2.Init(2022, 7, 6);
  d2.Print();
  return 0;
}
构造函数是一个特殊的成员函数,名字与类名相同, 创建类类型对象时由编译器自动调用 ,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次
注:构造函数类似Init函数的功能,它是特殊的成员函数,它的主要人物不是开空间创建对象,而是跟Init功能一样初始化对象。

📌 构造函数特性

  • 构造函数函数的函数名与类名相同。

  • 构造函数无返回值,但不是说返回值是void,不需要写void。

  • 对象实例化时编译器自动调用对应的构造函数。(与C语言的Init函数不同,这里是自动调用)

  • 构造函数可以进行函数重载。也就是说可以写多个构造函数,多种初始化方式,需要几个写几个。
class Date
{

public:
    Date() //无参构造
    {
        _year = 1;
        _month = 1;
        _day = 1;
    }

    Date(int year,int month,int day) //有参构造
    {
        _year = year;
        _month = month;
        _day = day;
    }

    Date(int year = 2, int month = 3, int day = 4) /全缺省
    {
        _year = year;
        _month = month;
        _day = day;
    }

private:
    int _year;
    int _month;
    int _day;
};
int main()
{ 
    Date d;            //无参构造 无参调用
    Date d1(2, 1, 1); //有参构造 对象+参数列表 有参调用
    return 0;
}

注:对于无参构造来初始化对象,规定是不能带括号,这样与函数声明分割开;而有参构造要带括号+参数列表。

在C++构造中我们更喜欢用全缺省的构造比较好用,但是此时要把无参构造屏蔽,因为他们两构成函数重载,但是无参调用时会存在歧义。

  • 默认构造:不传参就可以调用的构造并且默认构造函数只能有一个
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为
是默认构造函数。
  • 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦
    用户显式定义编译器将不再生成
编译器自动生成的构造函数对我们的成员有什么影响呢?
我们可以看到,当我们未显示定义构造函数时,编译器会自动生成一个构造函数,它对成员的处理如下:
1. 对于内置类型成员(如int/char/double/..),没有规定要不要处理,但是有的编译器会处理。
2. 对于自定义类型成员(class/struct..),会调用它的默认构造。因此一个类如果没有默认构
造会报错,因为此时它如果有自定义类型成员就需要调用这个自定义成员的默认构造。
注意: C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即: 内置类型成员变量在
类中声明时可以给默认值
class Date
{
private:
 // 基本类型(内置类型)
 int _year = 1970;
 int _month = 1;
 int _day = 1;
};

这种默认构造有什么意义呢?


class MyQueue
{
private:
Stack _pushst;
Stack _popst;

};

对于这种成员都是自定义类型的成员,此时自动生成的构造就可以对自定义类型成员调用默认构造,不需要我们自己动手。

总结:
1. 一般情况构造函数都需要我们显示实现(因为自定义类型的尽头是自定义类型,即自定义类型成员的成员最终还是自定义类型)。
2. 只有少数情况下,可以用编译器自动生成的构造函数(比如MyQueue这样的类成员都是自定义类型)。

🏠 析构函数

📌 析构函数概念

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。类似C语言我们对数据结构的Destroy,可以帮助我们解决我们有时忘记调用而造成内存泄露的问题。

📌 析构函数特性

  • 析构函数名是在类名前面加上字符~。
class Date
{
public:
   ~Date(); //析构    
};
  • 析构函数无参数无返回值。
  • 对象生命周期结束时,C++编译系统自动调用析构函数,当然他也能显示调用。

  • 一个类只能有一个析构函数(没有参数也就不支持重载)。若未显示定义,系统会自动生成默认的析构函数。

编译器自动生成的析构函数会对成员做什么呢?

我们可以看到当用户为显示定义析构函数时,会对它的成员做以下处理:

1. 对内置类型成员不做处理。

2. 对自定义类型成员去调用它的析构。

  • 不是所有的类都需要显示写析构函数,若类中有不属于对象的资源(如在堆上申请空间)则需要写析构进行清理。
//没有资源需要清理
class Date
{
 private:
  int _year;
  int _month;
  int _day;  
}

//有资源需要清理
class MyQueue
{
 private:
   Stack pushst;
   Stack popst;
}

总结:

1. 有资源需要清理,就需要写析构。(比如栈要在堆上申请空间,用完需要释放)。

2. 有两种情景不需要显示清理,用默认生成的析构就可以了。

a. 类中没有资源清理(比如Date中的年月日随着对象销毁就销毁了)

b. 类中内置类型成员没有资源需要清理,剩下都是自定义类型成员(比如MyQueue这个类,对于它的两个Stack类型成员析构时自动生成的析构直接调用他们的析构释放资源)。

  • 关于构造和析构的顺序

1. 全局对象优先于局部对象构造,局部对象按照出现顺序进行构造,无论是否为static。

2.析构的顺序按照构造的相反顺序进行析构(因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合先进后出的原则).

3.static会改变对象的生命周期,对象被static修饰后只会构造一次。在函数内的static对象会放在局部对象之后析构,但慢于全局对象析构。(因为全局在main函数之前构造)。若static对象在全局域中,则全局域对象的析构顺序同样参考他们先出现的顺序。

🏠拷贝构造函数

📌 拷贝构造函数概念

拷贝构造函数 只有单个形参 ,该形参是对本 类类型对象的引用 ( 一般常用 const 修饰 ) ,在用 已存 在的类类型对象创建新对象时由编译器自动调用
拷贝构造 / 拷贝初始化:用同类型的对象拷贝初始化。
拷贝构造两种写法:
class Date
{
public:
  

private:
   int _year;
   int _month;
   int _day;
};

 Date(const Date& d)   // 错误写法:编译报错,会引发无穷递归
 {
  _year = d._year;
  _month = d._month;
  _day = d._day;
 }

int main()
{
  Date d1; 
//拷贝构造
  Date d2(d1); 
  Date d3 = d1;
  return 0;
}

📌 拷贝构造函数特性

  • 拷贝构造函数是构造函数的一个重载形式,它的函数名也与类名相同,显示实现拷贝构造后编译器就不生成默认构造了
  • 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,
    因为会引发无穷递归调用。
调用拷贝构造就需要传参,我们这里传参是传值传参,C++规定自定义类型传值传参要调用拷贝构造,语法逻辑上循环往复造成无穷递归,但编译器会进行强制检查不允许这样用。
因此拷贝构造的参数必须用引用,引用是别名不需要拷贝,我们自定义类型对象在进行函数传参时,更推荐使用引用传参。
Date(Date* d)
{
  _year = d->_year;
  _month = d->_month;
  _day = d->_day;
}

若要完成拷贝用指针实现的构造也是可以实现的,但是规定拷贝构造参数是引用,而且引用比指针使用起来更方便。

Date(Date& d)   
 {
   d._year  = _year;
   d._month = _month;
   d._day   = _day;
 }

Date(const Date& d)   
 {
    _year = d._year;
   _month = d._month;
   _day = d._day;
 }

有时我们会有拷贝方向错误的错误,此时最好用const引用提前检测错误(这里是引用权限的缩小)。

  • 若未显式定义,编译器会生成默认的拷贝构造函数 默认的拷贝构造函数对象按内存存储按
    字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

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

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

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

  • 编译器自动生成的拷贝构造实现的是浅拷贝而不是深拷贝。

1. ,默认拷贝构造函数是按照值拷贝的,即将s1中内容原封不动的拷贝到s2中。因此s1和s2指向了同一块内存空间。

2. 当程序退出时,s2和s1要销毁。s2先销毁,s2销毁时调用折构函数,已经将0x11223344的空间释放了,但是s1并不知道,到s1销毁时,会将0x11223344的空间再释放一次,一块内存空间多次释放,肯定会造成程序崩溃。

解决方法:深拷贝

此时我们可以开一个跟s1指向空间一样大小的空间,再将里面的值一个一个拷贝到新开空间,再让s2指向这块空间。

总结:

1. 如果类没有管理资源,一般情况不需要写拷贝构造,用默认生成的拷贝构造就可以。(比如:Date类)。

2. 如果类中成员都是自定义类型成员,内置类型没有指向资源,用默认生成的拷贝构造就可以。(比如:MyQueue)

3.一般情况下,不需要写析构就不需要写拷贝构造,因为显示写析构和拷贝都是为了管理资源。

4.如果内部有一些指针或值指向资源,需要显示写析构函数释放,通常就需要写拷贝构造函数完成深拷贝。

  • 拷贝构造函数典型调用场景

1. 使用已创建对象来拷贝初始化新对象。

2.函数参数类型为类类型对象,一般对象传参时,尽量使用引用类型。

3.函数返回值类型为类类型对象,返回时根据实际场景,能用引用尽量使用引用

🏠 赋值运算符重载

📌 运算符重载

我们之前C语言对两个自定义类型的比较需要我们自己写一个函数,但是这样代码的可读性较差,为此C++引入了运算符重载,让自定义类型可以像内置类型一样可以直接使用运算符进行操作。

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

//比较日期的小于
bool compare(const Date& d1,const Date& d2)
{
   if(d1._year < d2._year)
      return true;
   else if(d1._year == d2._year)
   {
       if(d1._month < d2._month)
          return true;
       else if(d1._month == d2._month)
        {
            if(d1._year < d2._year)
                return true;
            return false;
        }
       else 
          return false;   
   }
   return false; 
} 

//运算符重载

bool opertaor<(const Date& d1,const Date& d2)
{
   if(d1._year < d2._year)
      return true;
   else if(d1._year == d2._year)
   {
       if(d1._month < d2._month)
          return true;
       else if(d1._month == d2._month)
        {
            if(d1._year < d2._year)
                return true;
            return false;
        }
       else 
          return false;   
   }
   return false; 
} 

cout << Compare(d1,d2)<<endl; //可读性差
cout << (d1 < d2) <<endl; //易于理解可读性强
运算 符重载是具有特殊函数名的函数 ,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名 : 关键字operator +  需要重载的运算符符号。

函数原型 :返回值类型 operator操作符 (参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 运算符重载的返回值与要重载的运算符有关,一个类要重载哪些运算符是看需求,看重载有没有价值和意义

比如Date类,d1 < d2 返回值应该为bool

++d,日期++,返回值还是Date

d1 - 100 自定义类型-内置类型 日期-天数返回值应该还是Date

而对于日期 x 日期并没有重载的意义。

  • 重载操作符必须有一个类类型参数,运算符重载是为了自定义类型而生的参数是自定义类型就没意义了
int operator-(int,int);//错误
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
  • .*   ::   sizeof   ?:   . 这五个运算符是不能被重载的
    class ob
    {
     void fcun(){}
    };//类ob内有函数func;
    
    typedef void(ob::*pobfunc)(); //成员函数指针类型
    
    int main()
    {
    
    pobfunc  fp = &ob::func;//定义成员函数指针p指向函数func
    
    ob temp;//定义ob类对象temp
    
    (temp.*fp)();使用对象temp加上.*运算符调用fp指向的成员函数
    
    }

    注:成员函数要加&才能取到函数指针,这里.*是帮助我们调用成员函数。

  • 运算符重载的两种调用方式:显示调用和转换调用
Date d1;
Date d2;
cout << operator==(d3,d4) <<endl;//显示调用
cout << (d3 == d4) <<endl; //转换调用

注:

1. 对于==优先级比流插入低,所以要加括号。

2.对于转换调用,本质上编译器还是会转换成operator==(d1,d2).

  • 运算符重载成全局函数,无法访问私有成员的问题
class Date
{
private:
  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;
}

由于operator==是在全局域重载的,但成员是私有的,在类外不能访问因此我们需要其他解决方法。

a. 提供这些成员的get或set

class Date
{
public:
  int Getyear()
  {
    return _year;
  }
//Getmonth() Getday()....
private:
	int _year;
    int _month;
    int _day;
};

b.使用友元

c.重载成成员函数(一般用这种)

  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐
    藏的this。毕竟运算符重载 参数个数要与重载的操作符的操作数一致。
class Date
{
public: 
    bool operator==(const Date& d1,const Date& d2);//参数个数为3 但==操作数应该为2
    bool operator==(const Date& d) //隐含的this 参数个数实际为2
    {
      //....
    }
private:
	int _year;
    int _month;
    int _day;
};

重载成成员函数后的调用,我们常用转换调用。

Date d1;
Date d2;
cout << d1.operator==(d4)<<endl; //显示调用成员函数
cout <<( d1 == d2 ) << endl;//本质还是转换成显示调用

注:如果此时又在全局重载还是会优先调用在类里面重载的,这也说明此时在全局重载没有意义。

两种调用汇编是一样的,都是call对应的函数,因此转换调用是等价于显示调用的。
而对于内置类型的比较是转换成对应的指令。
  • 运算符重载中,参数顺序和操作顺序是一致的。

比如cout << d1,要这样的话我们重载流插入操作符,第一个参数应该是ostream对象,第二个是Date类对象,后面实现日期类时我们详细讲解。

📌 赋值运算符重载

Date d1;
Date d2;
d2 = d1;

对于这段代码,d2==d1并不是调用拷贝构造,而是调用赋值运算符重载。对于赋值运算符重载我们需要注意以下几点:

  • 拷贝构造 vs 赋值重载

拷贝构造:一个已经存在的对象,拷贝给另一个要创建初始化的对象。

赋值重载:一个已经存在的对象,拷贝赋值给另一个已经要存在的对象。

  • 参数类型 const T&,传递引用可以提高传参效率。
赋值运算符重载函数的第一个形参默认是this指针,第二个参数由于是自定义类型传参,所以最好用引用提高效率,其次加上const进行修饰可以保证我们右操作数在函数体内不会被修改。
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值。

1.传值返回的话会调用拷贝构造生成临时对象(临时对象具有常性)降低效率,传引用可以提高效率。

2.同时,对于像d1 = d2 = d3这样的连续赋值场景,由结合性知,是从右向左结合,因此先对d2 = d3进行函数调用,如果此时返回值是void就不能继续给d1赋值了。

3.并且对于连续赋值d1 = d2 = d3,d2 = d3执行完毕后,我们如果是传值返回还要再对d2拷贝再赋值给d1,这是没必要的。

  • 传值返回 vs 传引用返回

传值返回

传引用返回

当传引用返回时,我们在函数内部返回一个局部对象是有风险的。

总结:

 1. 若返回对象生命周期到了,会析构销毁,此时用传值返回,不能用传引用返回,因为局部对象会随着函数栈帧释放而销毁,引用返回有销毁。

2. 若返回对象生命周期没到,不会析构(比如static对象)或不是返回局部对象,此时可以传引用返回。

//这里没返回局部或临时对象 传引用返回效率高
Date& operator=(const Date& d)
{
 _year = d._year;
 _month = d._month;
 _day = d._day;
 return *this;
}
  • 检测是否自己给自己赋值
Date& operator=(const Date& d)
{
 if(this != &d)
 {
  _year = d._year;
  _month = d._month;
  _day = d._day;
 }
 return *this;
}

比如d1 = d1,自己给自己赋值是没有必要的,所以在进行赋值操作前进行判断可以避免无效的赋值。

  • 返回*this :要符合连续赋值的含义。
赋值操作完毕之后需要返回赋值操作符的左操作数,也就是我们需要返回这个自定义类型实例的对象,此时由于函数体内可以用this指针访问左操作数,给我们开了这道口子满足需求,我们也就可以返回*this。
  • 赋值运算符只能重载成类的成员函数不能重载成全局函数(规定),因为它是默认的成员函数。
class Date
{
public:
 Date(int year = 1900, int month = 1, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 int _year;
 int _month;
 int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
 if (&left != &right)
 {
 left._year = right._year;
 left._month = right._month;
 left._day = right._day;
 }
 return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。
  • 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
1. 默认的赋值运算符重载对于内置类型成员是直接赋值,以值的方式逐字节拷贝。
2. 默认的赋值运算符重载对于 自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
默认赋值重载意义:
1. 跟拷贝构造类似,对于像Date和MyQueue这样的类默认生成的赋值够用;而对于类似Stack这样管理资源的类就需要我们自己显示实现赋值重载。
2.  如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
  • 对于内含引用成员的class,重载赋值操作符并无意义,因为引用不可改变指向,指向其他对象。

🏠 const成员函数

📌 const成员函数

将const修饰的“成员函数”称之为const成员函数 const修饰类成员函数 实际修饰该成员函数隐含的this指针 表明在该成员函数中不能对类的任何成员进行修改
class Date
{
public:
    void Print()const
    {
        cout << _year << " " << _month << " " << _day << endl;
    }

    void Print(const Date* this) //编译器处理
    {
        cout << this->_year << " " << this->_month << " " << this->_day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

📌 const 与 非const比较

【面试题】

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

2. 非const对象可以调用const成员函数吗?
3. const成员函数内可以调用其它的非const成员函数吗?
4. 非const成员函数内可以调用其它的const成员函数吗?
回答:
1. const对象的指针类型是被const修饰的对象指针,而非const成员函数的参数是没有被const修饰的this指针,此时传入被const修饰的对象,属于权限放大,这是不行的。
2. 非const对象的指针类型是未被const修饰的对象指针,而const成员函数的参数是被const修饰的this指针,此时传入非const对象,属于权限的缩小,函数正常调用。
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
 {
  return this ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!
class Date
{ 
public :
 Date* operator&()
 {
  return nullptr ;
 }
 const Date* operator&()const
 {
  return (const Date*)0xffffffff ;
 }
private :
 int _year ; // 年
 int _month ; // 月
 int _day ; // 日
};

🏠 Date类的实现

📌 日期类大小比较

    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;

日期类大小关系需要重载的运算符有6个,但其实我们只需重载其中两个(比如opertaor<和opeartor==)即可。

  • operator < 

判断日期哪个小逻辑很简单,先比较年,年小的小;年相等比月,月小的小;月相等的再比较日,日小的日期小。

参考代码:

bool Date::operator<(const Date& d)
{
	if (_year < d._year)
	{ //年小直接日期小
		return true;
	}
	else if (_year == d._year)
	{//年等再比月
		if (_month < d._month)
		{
			return true;
		}
		else if (_month == d._month) //月等再比日
		{
			return _day < d._day;
		}
	}
	return false;
}
  • operator ==

判断两个日期相等只需要判断他们的年月日是不是都相等。

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

小于等于只要判断这个日期是否是小于或等于另一个日期即可,我们直接复用前两个实现的小于和等于。

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

我们知道大于的对立事件,所以取<=的逻辑反就是大于,此时可以复用上一个实现的<=。

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

大于等于的对立事件是小于,所以我们直接复用<即可。

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

不等于的对立事件是等于,我们直接复用==即可。

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

📌 日期 += 天数

1.返回值类型:日期+=天数还是日期,同时为了提高效率我们可以使用传引用返回

2. +=逻辑:当前月的天数加上我们传的天数,此时可能超出当前月的总天数,此时我们要知道超出多少,月份就要++;同时如果超出天数过多,可能还要year++。

3.GetmonthDay:既然要知道超出当前月多少天,我们就需要知道每个月的天数,我们可以在类里面定义这个函数不做声明定义分离,此时它是内联的正好满足我们频繁调用这个函数的需要。

4.对于获取月份天数,我们可以先用数组存好余年情况下每个月的天数,由于需要反复使用这个数组,我们最好把这个数组设置为static,最后还要对闰年情况的二月进行单独判断。

参考代码:

class Date
{
//...
  int GetMonthDay(int year, int month)
{ 
  assert(month > 0 && month < 13);
  static int monthDayArray[13] = { -1, 31, 28, 31,30,31,30,31,31,30,31,30,31};//下标从0开始 
                                                                               // 不好处理
// 闰年
  if (month == 2 && (year % 4 == 0 && year % 100 !=0) || (year % 400 == 0))
  {
    return 29;
  }
  else
  return monthDayArray[month];
}

//...
};


Date& Date::operator+=(int dat)
{
  _day += day;
  while(_day > GetMonthDay(_year,_month))
  {
     _day -= GetMonthDay(_day,_month); //满足当前月后剩下的天数
     ++_month; //超出当前月
     if(_month == 13)
     {
        ++_year;  
        _month = 1;  
     }
  }
  return *this;
}

📌 日期 + 天数

1. 函数返回值:日期+天数得到的是一个新日期,所以我们需要再函数体内创建一个新对象再运用跟+=一样的处理多出来天数的逻辑,但由于返回局部对象有风险,因此我们最好用传值返回。

2.既然逻辑一样,那我们可以再次实现复用+=来重载operator+.

Date Date::operator+(int day) const
{
	Date temp(*this);//拷贝
	temp += day; //复用
	return temp;
}

既然逻辑一样,那是否可以先实现+,再让+=复用+呢?

Date Date::operator+(int day) const
{
	Date temp(*this);//拷贝
	temp._day += day;
  while(temp._day> GetMonthDay(temp._year,temp._month))
  {
     temp._day-= GetMonthDay(temp._day,temp._month); //满足当前月后剩下的天数
     ++temp._month; //超出当前月
     if(temp._month == 13)
     {
        ++temp._year;  
        temp._month = 1;  
     }
  }
	return temp;
}

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

这种复用方式当然是可以的只不过涉及多次拷贝(复用的+是传值返回)效率较低,因此不推荐。

📌 日期 -= 天数

1.返回值类型:对于日期 -= 天数还是日期,因此返回Date&.

2.逻辑:如果当前月天数减去所传天数够它减就没问题;如果所要减去天数超出当前日期的天数,此时需要看要回退到哪个月;如果要减去天数过多,此时看是否要回退到上一年。

参考代码:

Date& Date::operator-=(int day)
{
	_day -= day;
	//此时该月的减完了
	while (_day < 0)
	{
		 //用上一个月的来补
		--_month;
		if (_month == 0)
		{
			_year--;
			_month = 12;
		}
		_day += GetMonthday(_year, _month);
	}
	return *this;
}

但是我们需要注意一个问题是:如果我们减去天数是负的,此时相当于+=;类似的,如果我们加上天数是负的,此时相当于是-=。我们需要连同前面+=做出调整。

Date& Date::operator+=(int 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;
}

Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return (*this) += -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 temp(*this);
	temp -= day;
	return temp;
}

📌 前置++

前置++是先加后用,我们可以先复用+=给日期类对象+1,再返回*this.

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

📌 后置++

重载前置++之前我们需要理清函数重载与运算符重载之间的关系。

1.函数重载:可以让函数名相同,参数不同的函数存在。

2.运算符重载:让自定义类型可以用运算符,并且控制运算符行为,增强可读性。

3.两者之间并无关系,但是多个同一运算符重载可以构成函数重载。如下

Date operator-(int day);

int operator-(const Date& d);

对于前置++和后置++,他们用的都是同一运算符所以为了区分他们构成重载,设计者强行给后置++的重载增加了一个int形参,只是单纯为了区分。

// 这里不需要写形参名,因为接收值是多少不重要,也不需要用
// 这个参数仅仅是为了跟前置++构成重载区分
Date Date::operator++(int)
{
	Date temp(*this);
	*this += 1;
	return temp;
}

注:后置++是先用后加,所以需要一个新对象保存没加之前结果再+1,由于返回临时对象所以用传值返回。

📌 日期-日期

1.返回值类型:日期-日期得到的是天数,返回值应该是int。

2. 逻辑:我们应该先确定哪个日期比较大max,哪个日期比较小min,可以使用假设法和复用我们之前的大小比较。接着可以让min不断++到max,所加的天数就是多出来天数。

int Date::operator-(const Date& d) const
{
	int flag = 1;
	Date max = *this;
	Date min = d;
	int n = 0;
	if (*this < d)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	while (min != max)
	{
		min++;
		n++;
	}
	return n*flag;
}

📌 流插入重载

  • 内置类型使用流插入
int i = 0 ;
double d = 1.1;
cout << i << " "<< d <<endl;

我们平常使用的cou其实是个ostream对象,为什么可以直接cout << 内置类型对象呢?

其实是因为库里面重载好了对于内置类型使用流插入操作符的函数。

  • 自定义类型
void Date::operator<<(ostream& out)
{
  out << _year << "年" << _month << "月" << _day << "日"<<endl;
} 

d1.operator<<(cout);//传ostream对象
d1 << cout;

有同学可能会好奇为什么转换调用时,Date类是做左操作数?

那是因为在运算符重载中,参数顺序和操作数顺序是一致的。我们将流插入重载成成员函数,它的第一个参数是隐含的this,第二个是ostream对象,因此我们调用时也应该满足这个顺序。因此operator<<重载成成员函数是可以的,不过使用时不符合正常逻辑,建议重载成全局函数。

在重载成全局前我们需要知道我们成员变量一般是设置成私有的,我们若想在全局访问他们,除了写一个Get或Set,我们可以将流插入重载设置为友元。友元即这个类的朋友,也可以访问这个类成员。


class Date
{
//...
friend void operator<<(ostream& out, const Date& d);

}

void operator<<(ostream& out, const Date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;	
}

还有不足的是,上面返回值是void的话,如果我们想连续打印多个日期类对象,由于是从左向右结合,所以我们需要返回一个ostream对象继续进行打印。

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

📌 流提取重载

对于流提取,也需要注意它需要重载成全局并设置为友元,同时为了支持连续输入,应该返回istream对象。更多的,流提取重载还需要注意判断输入日期是否合法。



bool Date::CheckDate()
{
	if (_month < 1 || _month > 12 || _day <1 || _day > GetMonthday(_year, _month))
	{
		return false;
	}
	return true;
}



istream& operator>>(istream& in, Date& d)
{
	cout << "请依次输入年月日:>";
	in >> d._year >> d._month >> d._day;

	if (!d.CheckDate())
	{
		cout << "日期非法" << endl;
	}
	return in;
}

📌 构造函数

对于构造函数我们建议采取全缺省的方式,同时还要注意初始化的日期是否合法。

Date::Date(int year, int month, int day)
{
	// 检查日期的合法性
	if(CheckDate())
	{
		_year = year;
		_month = month;
		_day = day;
	}
	else
	{
		cout << "非法日期" << endl;
		cout << year << "年" << month << "月" << day << "日" << endl;
	}
}

📌 何时设置const成员函数

根据我们前面的知识,我们知道对于const成员函数是不能修改成员的。

1. 不是所有的成员函数都需要设置为const,比如对于+=,我们是要改变日期,此时设置为const就不合理了。

2. 对于不需要修改成员变量的成员函数,此时能设置为const就设置为const,此时const对象和非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;

总结:本节我们主要认识了类的6个默认成员函数特性,以及何时需要他们还有const成员函数,同时通过Date类的实现对这6个特殊成员函数有了更深刻理解。

  • 29
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 14
    评论
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值