C++初阶学习-类和对象(中篇)

文章目录

1.类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。
空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

class Date{};//空类

在这里插入图片描述

2.构造函数

2.1构造函数的概念

对于下面这个类:

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;
};

注意到,对于这个类,如果我们要实例化对象,就必须得调用Init函数来初始化对象,如果忘记初始化就会导致崩溃(出现随机值),因此我们需要有一个方法来保证对象一定会被初始化,使得对象在创建时,信息就会被设置进去。这就引出我们的默认成员函数——构造函数
构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。

2.2构造函数的特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
其特征如下:

2.2.1.函数名与类名相同。

2.2.2 无返回值。

注意无返回值也不用写void

2.2.3 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

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

注意:这里的对应的意思是:如果我们有显示定义构造函数,则编译器会调用我们写的,并不再自己生成,若我们没有显示定义,则编译器则会自己生成并调用。

2.2.5构造函数可以重载。

对于这一点,我们需要知道,虽然构造函数是可以重载的,但是默认的构造函数只能存在一个!

我们需要知道:有3类默认构造函数

在这里插入图片描述

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。
注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

	//不需要写Init了,调用太麻烦
	/*void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/
	//1.全缺省的带参构造
	Date(int year = 1,int month = 1,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//2.无参构造
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	//3.编译器自动生成的构造函数
	
	//一般采用全缺省的带参构造,非常好用,可以选择性的传参数或不传,但这两种构造函数不能同时存在。

2.2.6分析默认生成的构造函数

现在我们知道,我们可以显示定义构造函数,并且构造函数也可以重载。
若我们没有写上述的构造函数(即没有显示定义),则编译器会自动生成默认成员函数,下面我们来了解编译器自动生成的默认成员函数到底好不好用呢?
我们通过一段代码来分析一下:

#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;
}

最终运行结果如下:
在这里插入图片描述
这样看来,我们编译器自动生成的默认构造函数好像没什么用,因为对象通过生成的构造函数初始化后还是随机值。真是如此吗?
虽然确实是如此,但是是由其特性引起的:
C++把类型分成内置类型(基本类型)和自定义类型。
内置类型就是语言提供的数据类型,如:int/char…,指针等等
自定义类型就是我们使用class/struct/union等自己定义的类型

而默认生成的构造函数他的功能是:
1.对于内置类型不做处理
2.对于自定义类型成员,会去调用他的默认构造函数

例如下面的代码:

class Time
{
public:
 Time()
 {
	 cout << "Time()" << endl;
	 _hour = 0;
	 _minute = 0;
	 _second = 0;
 }
private:
	 int _hour;
	 int _minute;
	 int _second;
};
class Date
{
private:
	 // 基本类型(内置类型)
	 int _year;
	 int _month;
	 int _day;
	 // 自定义类型
	 Time _t;
};
int main()
{
 	Date d;
 	return 0;
}

运行结果如下:
在这里插入图片描述
从结果我们就可以看出,默认生成构造函数会调用自定义类型的默认构造函数。因此:默认生成构造函数的作用体现在部分场景。
现在我们就能解释为什么还是随机值了,因为对于内置类型,默认生成的构造函数压根就不管呀!
因此,有必要写显示定义时十分建议写全缺省的构造函数。
而针对默认生成构造函数对内置类型不处理的情况,C++11打了一个补丁:内置类型成员变量在类中声明时可以给默认值。
我们可以对内置类型做如下处理:

private:
	int _year=1;
	int _month=1;
	int _day=1;

注意:这里不是对内置类型直接进行定义,这里仍旧是成员变量声明,这里相当于给成员变量赋了一个缺省值,如果编译器检测到没有显示定义构造函数时,会调用缺省值进行初始化,若有显示定义则缺省值无任何影响。(这一块是为了弥补默认生成成员函数的缺点所打的补丁)

3.析构函数

3.1 析构函数的概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
对于我们前面写的日期类,我们在一个函数中实例化对象时,首先先会建立该函数的栈帧,并把该实例化对象通过压栈的方式放入栈帧空间,当函数调用完毕,出了函数作用域时,函数的栈帧空间会销毁,因此实例化对象的空间也会被随之销毁,此过程由编译器完成。
但是,要是遇到这样的场景,情况就不一样了:
在实例化对象时,我们需要用到动态开辟空间,如实现栈时,栈的空间是通过动态开辟来的,因此这种情况就算出了函数作用域,这块空间也不会被销毁,因此析构函数的作用就体现出来了:为了防止动态开辟的空间忘记销毁,我们把需要free的空间写在析构函数里,在对象生命周期结束时,析构函数会由编译器自动调用来帮助我们销毁,这样保证了不会出现内存泄漏的问题。

3.2析构函数的特性

3.2.1 析构函数名是在类名前加上字符 ~。

class Date
{
public:
		~Date()
		{...}
};

3.2.2 无参数无返回值类型。

无返回值也不需要加void。

3.2.3 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载

由于析构函数不能重载,因此析构函数只有显示定义的与默认生成的两种。

3.2.4 对象生命周期结束时,C++编译系统系统自动调用析构函数

有效避免了出现内存泄漏问题。

3.2.5分析默认生成的析构函数

类似地,默认生成析构函数与默认生成构造函数有着相同特点

1.内置类型不做处理
2.自定义类型成员会去调用其析构函数

我们还是用代码来验证其特点:

class Time
{
public:
	//析构函数
	~Time()
	{
		cout << "~Time()" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

运行结果:
在这里插入图片描述
此时我们发现编译器是实实在在的调用了Time类型的析构函数。
在main方法中根本没有直接创建Time类的对象,为什么最后会调用Time类的析构函数?
因为:main方法中创建了Date对象d,而d中包含4个成员变量,其中_year, _month,_day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而_t是Time类对象,所以在d销毁时,要将其内部包含的Time类的_t对象销毁,所以要调用Time类的析构函数。但是:main函数中不能直接调用Time类的析构函数,实际要释放的是Date类对象,所以编译器会调用Date类的析构函数,而Date没有显式提供,则编译器会给Date类生成一个默认的析构函数,目的是在其内部
调用Time 类的析构函数,即当Date对象销毁时,要保证其内部每个自定义对象都可以正确销毁
main函数中并没有直接调用Time类析构函数,而是显式调用编译器为Date类生成的默认析构函数
注意:创建哪个类的对象则调用该类的构造函数,销毁那个类的对象则调用该类的析构函数。

总结:默认生成析构函数会调用自定义类型成员的析构函数完成销毁工作。

3.2.6对象的销毁顺序

先构造的后析构,后构造的先析构

何出此言?
我们前面提到,在函数中,实例化对象时,是要对对象进行压栈的,因此栈帧销毁时,顺其自然的后面压入栈的对象的内存空间先被回收,一直到栈底。
在这里插入图片描述
但是要注意static对象的存在,因为static改变了对象的生存作用域,需要等待程序结束时才会析构释放对象。
析构顺序:局部对象(先构造的后析构,后构造的先析构)->静态对象->全局对象

4.拷贝构造函数

4.1拷贝构造函数的概念

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

#include <iostream>
using namespace std;
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;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2023, 4, 11);
	Date d2(d1); // 用已存在的对象d1创建对象d2
	Date d3=d1; // 两种方式都行

	return 0;
}

4.2拷贝构造函数的特性

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

都与类名相同,但形参不同。

4.2.2 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

如何理解传值引发无穷递归调用呢?
在这里插入图片描述
我们知道,形参是对实参的拷贝,因此传参时,若使用了传值传参,形参又一次调用拷贝构造函数去拷贝实参…以此类推,显而易见推不下去了…无限递归了。
注意:在后续的自定义类型传参时,如果不用改变对象,则建议都用传引用传参,因为就算传值传参能够成功的话也是会有消耗的。

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

我们用以下代码来验证

#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(2021, 5, 30);
	Date d2(d1); // 用已存在的对象d1创建对象d2
	d1.Print();
	d2.Print();
	return 0;
}

运行结果:
在这里插入图片描述
可见就算没有显示拷贝构造代码,拷贝也成功了,那是不是说拷贝构造就没有必要去显示定义了呢?

4.2.4分析默认生成拷贝构造函数

默认生成拷贝构造函数特点就与前两个不同了:

1.内置类型按照字节方式直接拷贝
2.自定义类型调用其拷贝构造函数完成拷贝。

那这样是不是就更不需要去显示定义了呢?编译器已经把该干的事都干完了呀!
那就大错特错了!这只是冰山一角,我们来看下面的代码:

//我们来实现一个栈,若没有学过也没关系,我们来看解析
typedef int DataType;
class Stack
{
public:
		//构造
 Stack(size_t capacity = 10)
 {
 	//注意这个啦,是一块动态开辟的空间哟
	 _array = (DataType*)malloc(capacity * sizeof(DataType));
	 if (nullptr == _array)
	 {
	 perror("malloc申请空间失败");
	 return;
	 }
	 _size = 0;
	 _capacity = capacity;
 }
		 //入栈
 void Push(const DataType& data)
 {
	 // CheckCapacity();
	 _array[_size] = data;
	 _size++;
 }
	 //析构
 ~Stack()
 {
	 if (_array)
	 {
		 free(_array);
		 _array = nullptr;
		 _capacity = 0;
		 _size = 0;
 	}
 }
private:
	 DataType *_array;
	 size_t _size;
	 size_t _capacity;
};
int main()
{
	 Stack s1;
	 s1.Push(1);
	 s1.Push(2);
	 s1.Push(3);
	 s1.Push(4);
	 Stack s2(s1);
	 return 0;
}

在这里插入图片描述

  1. s1对象调用构造函数创建,在构造函数中,默认申请了10个元素的空间然后里面存了4个元素1 2 3 4
  2. s2对象使用s1拷贝构造,而Stack类没有显式定义拷贝构造函数,则编译器会给Stack类生成一份默认的拷贝构造函数,默认拷贝构造函数是按照值拷贝的,即将s1中内容原封不动的拷贝到s2中。因此s1和s2指向了同一块内存空间
  3. 当程席退出时,s2和s1要销毁。s2先销毁,s2销毁时调用析构函数,已经3将0x11223344的空间释放了,但是s1并不知道,到s1销毁时,会将0x11223344的空间再释放一次,一块内存空间多次释放,肯定会造成程序崩溃。

总结:
对于无需动态开辟空间的,默认生成拷贝构造就够用(浅拷贝)
但对于需要动态开辟空间的,需要完成显示定义
(深拷贝)

到此,三种基本默认成员函数讲完了,我们用一段代码来分析其中的逻辑过程来巩固一下:

class Date
{
public:
 Date(int year, int minute, int day)
 {
	 cout << "Date(int,int,int):" << this << endl;
 }
 Date(const Date& d)
 {
	 cout << "Date(const Date& d):" << this << endl;
 }
 ~Date()
 {
 	cout << "~Date():" << this << endl;
 }
 private:
	 int _year;
	 int _month;
	 int _day;
};
Date Test(Date d)
{
	 Date temp(d);
	 return temp;
}
int main()
{
	 Date d1(2022,1,13);
	 Test(d1);
	 return 0;
}

在这里插入图片描述

5.赋值运算符重载

5.1 运算符重载

对于我们上述实现的日期类,若我们想实现日期的运算,比如计算2023年4月11号的100天后的日期是多少,我们一般会想到用函数来处理,但是,用函数处理太麻烦了,若我们频繁的使用,则代码可读性大大降低,于是我们就想,我们是否能让自定义类型像内置类型一样,进行运算符运算呢?因此,想让编译器知道如何对自定义类型运算,我们使用运算符重载。

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

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

  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型参数
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  4. 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  5. .* :: sizeof ?: . 注意以上5个运算符不能重载。
    我们在重载函数时会遇到这种情况:
    我们会将重载函数放在全局中,这样会面临一个问题在这里插入图片描述
    其实还有一种方式可以解决:友元,后续在类和对象—下中会提到。在这里我们采用解决方法二。上代码:
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    // bool operator==(Date* this, const Date& d2)
   // 这里需要注意的是,左操作数是this,指向调用函数的对象
   bool operator==(const Date& d2)
   {
       return _year == d2._year
      		 && _month == d2._month
             && _day == d2._day;
   }
    //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;
//}
void Test()
{
    Date d1(2018, 9, 26);
    Date d2(2018, 9, 27);
    cout << (d1 == d2) << endl;
    //在编译器中,这句代码会被转化成:
    //cout<<d1.opertor==(&d1,d2)<<endl;
}

5.2赋值运算符重载

我们以重载“=”来作为参考

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //d1 = d2 = d3;支持连续赋值
    Date& operator=(const Date& d2)
    {
        if(this!=&d2)
        {
            _year = d2._year;
            _month = d2._month;
            _day = d2._day;
        }
        return *this;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1(2023, 4, 11);
    Date d2;
    d2 = d1;
    return 0;
}

看完以上代码,我们有了个大概的框架后,我们来拆分其格式:
赋值运算符重载格式

  1. 参数类型:const T&,传递引用可以提高传参效率
  2. 返回值类型:T&,返回引用可以提高返回的效率, 有返回值目的是为了支持连续赋值
  3. 检测是否自己给自己赋值
  4. 返回*this :要复合连续赋值的含义
  1. 对于参数类型,我们要记住左操作数是第一个形参(this),右操作数是第二个形参(引用做形参,提高效率,拷贝有消耗),由于函数体不会改变修改右操作数,加上const修饰保护对象,也能提高接受度(权限缩小)
  2. 对于返回值类型,为了使此赋值操作符支持连续赋值(参考内置类型 i=j=10 ),我们需要返回左操作数,同样地,我们传引用返回,减少消耗。
  3. 对于检查是否给自己赋值,若自己给自己赋值那么是没有意义的,避免编译器做不必要的功夫我们赋值前先判断。
  4. 对于返回*this,与第2点对应,*this便是左操作数。

熟悉完赋值运算符重载的格式后,我们知道,既然作为默认成员函数,那么编译器肯定会在没有显示定义的情况下自动生成一个,下面我们来分析一下。

5.2.1分析默认生成赋值运算符重载函数

先说一个结论:赋值运算符只能重载成类的成员函数不能重载成全局函数
原因:由于默认生成赋值运算符重载函数的存在,如果编译器在类内没有发现赋值运算符的成员函数后便自动生成一个,此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。这也侧面说明了默认生成的存在。
那么,此默认赋值运算符存在有什么特点呢?

  1. 对于内置类型,会完成值拷贝操作(按字节拷贝)
  2. 对于自定义类型,会调用其赋值运算符重载函数
    (与拷贝构造的特点一致)

那么同样的,是不是就代表不需要去显示定义了呢?
不是的,比如遇到以下场景时:

typedef int DataType;
class Stack
{
public:
    Stack(size_t capacity = 10)
    {
        _array = (DataType*)malloc(capacity * sizeof(DataType));
        if (nullptr == _array)
        {
            perror("malloc申请空间失败");
            return;
        }
        _size = 0;
        _capacity = capacity;
    }
    void Push(const DataType& data)
    {
        // CheckCapacity();
        _array[_size] = data;
        _size++;
    }
    ~Stack()
    {
        if (_array)
        {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }
private:
    DataType* _array;
    size_t _size;
    size_t _capacity;
};
int main()
{
    Stack s1;
    s1.Push(1);
    s1.Push(2);
    s1.Push(3);
    s1.Push(4);
    Stack s2;
    s2 = s1;
    return 0;
}

以上代码是否还正常运行呢?答案是否定的
在这里插入图片描述
会有两个严重的问题:

  1. 由于s1和s2都是动态申请的空间,如果以浅拷贝完成赋值操作,则s1内指针指向的空间地址将会原封不动的被s2所指向,而s2原来所指向的空间则会被丢失,导致内存泄漏。
  2. 等到对象生命周期结束时,编译器自动调用析构函数,由于s2和s1指向同一块空间,所以这块空间会被析构两次,引起程序崩溃。

6.const成员

const成员函数

将**const修饰的“成员函数”**称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

class
{
public:
		void Display()const
		{
		...
		}
}

我们通过把const加在需要改变的函数之后。
编译器会做如下处理:
在这里插入图片描述
那么const成员函数有什么用呢?

  1. 保证了成员函数内部不会修改成员
  2. 使得可接受度变高,无论是const对象还是非const对象都可以调用这个成员函数。

请思考下面的几个问题:

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

Answer:

  1. 不可以,非const成员函数所指向的内容和所指向的对象都能被修改,而const对象对所指向的内容是不允许修改的,这样导致了权限放大。
  2. 可以,所指向内容可以被修改转化为不能被修改,属于权限缩小。
  3. 不可以 ,const成员函数内this指针已经被const所修饰,属于const对象,调用非const成员函数时是属于权限的放大。
  4. 可以,非const成员函数内的this指针属于非const对象,调用const成员函数属于权限缩小。

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

除了构造,析构,拷贝构造,运算符重载,还有两个默认成员函数。
取地址及const取地址操作符重载函数也是编译器会自动生成的默认成员函数,这两个默认成员函数一般不用重新定义 ,编译器默认生成的就够用。
除了极少可能的特殊场景,这两个都不用自己写。

class Date
{
public :
 Date* operator&()
 {
	 return this ;
 }
 const Date* operator&()const
 {
	 return this ;
 }
private :
	 int _year ; // 年
	 int _month ; // 月
	 int _day ; // 日
};

8.实现一个完整的日期类

我们先把类的总体框架写出来(先声明成员函数),再一个一个实现:

#pragma once
#include<iostream>
#include<stdbool.h>
using namespace std;
class Date
{
public:
	//构造函数
	Date(int year=1,int month=1,int day=1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//运算符重载
	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;
	//赋值运算符重载
	//d1 + 100 || d1+=100
	Date operator+(int day) const;
	Date& operator+=(int day);
	// ++d1;
	// d1++;
	// 直接按特性重载,无法区分
	// 特殊处理,使用重载区分,后置++重载增加一个int参数跟前置构成函数重载进行区分
	Date& operator++(); // 前置
	Date operator++(int); // 后置

	// d1 - 100|| d1-=100
	Date operator-(int day) const;
	Date& operator-=(int day);
	
	Date& operator--(); // 前置
	Date operator--(int); // 后置

	// d1 - d2
	int operator-(const Date& d) const;
private:
	int _year;
	int _month;
	int _day;
};

8.1日期类的构造函数

首先是构造函数,由于构造函数会被频繁调用,因此直接在类内定义做内联函数,但若只是上述那样实现太过草率,我们还得考虑当传参不小心传错的情况,即日期是否合法,因此我们对构造函数进行了优化:

	//频繁调用,构造函数类内定义作为内联
	Date(int year=1,int month=1,int day=1)
	{
		_year = year;
		_month = month;
		_day = day;
		assert(CheckDate());
	}
	//和构造函数一起会被频繁使用,因此作为内联展开
	//获取每个月对应的天数
	int GetmonthDay(int year, int month)
	{
		//	会被频繁调用,避免每次使用都要开辟数组空间,直接用static修饰放在静态区
		static int days[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
		int day = days[month];
		//	小细节:先判断是不是2月才有必要去判断是不是闰年
		if (month==2 && (year % 4 == 0 && year % 10 != 0) || (year % 400) == 0)
		{
			day += 1;
		}
		return day;
	}
	//和构造函数一起会被频繁使用,因此作为内联展开
	//检查日期是否合法
	bool CheckDate()
	{
		if (_year >= 1
			&& _month >= 1
			&& _month < 13
			&& _day>0
			&& _day <= GetmonthDay(_year, _month))
		{
			return true;
		}
		else
			return false;
	}

以上函数都放在类中定义,编译器会自动识别为内联函数

8.2日期类的运算符的重载(==,!= , > , >= , < , <=)

对于哪些运算符可以重载,我们需要思考的是,哪些运算符对于这个类是有实现意义的

//先实现==,  !=可以直接复用
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);
}
//先实现>, >=以及<=可以服用
bool Date::operator>(const Date& d) const
{
	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;
	}
	else
	{
		return false;
	}
}

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);
}

值得注意的是,我们其实就实现了 == 以及 > 两个函数,而其他函数可以直接复用这两个函数,这样一来就大大提高了效率(前提是两个函数都写对)

8.3赋值运算符重载

同样地,我们需要思考哪些赋值操作是对日期类有意义的,经过整理我们可以实现的有:

	//赋值运算符重载
	//d1 + 100 || d1+=100
	Date operator+(int day) const;
	Date& operator+=(int day);
	// ++d1;
	// d1++;
	// 直接按特性重载,无法区分
	// 特殊处理,使用重载区分,后置++重载增加一个int参数跟前置构成函数重载进行区分
	Date& operator++(); // 前置
	Date operator++(int); // 后置

	// d1 - 100|| d1-=100
	Date operator-(int day) const;
	Date& operator-=(int day);
	
	Date& operator--(); // 前置
	Date operator--(int); // 后置

	// d1 - d2
	int operator-(const Date& d) const;
private:
	int _year;
	int _month;
	int _day;
};
  • 实现 : 日期+=天数 和 日期+天数
    +=是加在对象左操作数本身,因此用传引用返回即可
Date& Date::operator+=(int day)
{
	//day<0代表要减去正的天数,因此复用-=
	if (day < 0)
	{
		*this -= -day;
	}
	_day += day;
	while (_day>GetmonthDay(_year,_month))
	{
		_day -= GetmonthDay(_year, _month);
		_month += 1;
		if (_month > 13)
		{
			_month = 1;
			_year += 1;
		}
	}
	return *this;
}

+的左右操作数都不变,因此用传值返回(若传引用出了作用域临时变量会被销毁)
并且+用+=复用,减少消耗(传值返回会有销耗)

Date Date::operator+(int day) const
{
	Date ret = *this;
	ret += day;
	return *this;
}
  • 实现前置++和后置++
    由于前置++和后置++都使用++符号,因此直接按特性重载,无法区分
    特殊处理,使用重载区分,后置++重载增加一个int参数跟前置构成函数重载进行区分。
    前置:返回+之后的值
    后置: 返回+之前的值
// ++d1;
// d1++;
// 直接按特性重载,无法区分
// 特殊处理,使用重载区分,后置++重载增加一个int参数跟前置构成函数重载进行区分
// 前置
Date& Date::operator++()
{
	*this += 1;
	return *this;
} 
// 后置
Date Date::operator++(int)
{
	Date temp = *this;
	*this += 1;
	return temp;
} 
  • 实现 : 日期-=天数 和 日期-天数
    注意点跟+=和+差不多,不多赘述
// d1 - 100
Date Date::operator-(int day) const
{
	Date ret = *this;
	ret -= day;
	return *this;
}
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}
	_day -= day;
	while (_day <= 0)
	{
		_month -= 1;
		_day += GetmonthDay(_year, _month);
		if (_month < 0)
		{
			_month = 12;
			_year -= 1;
		}
	}
}
  • 实现前置–和后置–
Date& Date::operator--() // 前置
{
	*this -= 1;
	return *this;
}
Date Date::operator--(int) // 后置
{
	Date temp = *this;
	*this -= 1;
	return temp;
}
  • 实现日期减去日期
    有两种实现思路:
  1. 让这两个日期与减去同一个日期(可以是1年1月1日)尽量小的日期,然后在让两个差值相减
  2. 用小的日期一直加1加到与大的日期相等,同时配上一个计数器即可(用这种)
// d1 - d2
int Date::operator-(const Date& d) const
{
	//大减小,用flag来控制正负
	Date max = *this;
	Date min = d;
	int flag = 1;
	//小减大
	if (*this < d)
	{
		max = d;
		min = *this;
		int flag = -1;
	}
	int count = 0;
	while (max != min)
	{
		min++;
		count++;
	}
	return count * flag;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值