• 详解c++---类和对象(三)


    拷贝构造函数

    为什么会有该函数

    我们先来看一个场景:我们创建了一个对象:Date d1(2022,9,22)然后想再创建一个对象但是这个对象的数据得是d1的后七天,那我们该如何去做呢?首先第一步我们得再创建一个对象d2并且该对象的数据得跟d1的一模一样,然后我们再把d2的数据加上7天,这样就能得到我们想要的结果。那有些同学就说啊为什么要这么做啊?我们直接在创建对象的时候初始化为2022,9,29不就够了吗?那有这种想法的同学就在思考问题的时候有点不周全了,我们这是数据小的时候可以直接通过人脑来计算出来,如果我们这里的数据十分的大呢?我们想要计算100000天之后的年月份是多少的话,你还能直接初始化的时候给数据吗?答案是显然不行的,所以我们得先把原数据复制一份然后再对该数据进行接下来的一系列操作,那么这里就会出现一个问题,我们这里的数据是如何来复制的?是不是得一个一个的输入数据啊,我们这里的数据比较少,没用什么时间成本,那如果一个类中的数据非常的多呢?有100个数据呢?你在初始化的时候还是一个一个的填写吗?所以为了解决这个问题我们的c++就给了一个东西叫做拷贝构造他就可以完美的解决我们在赋值上出现的问题,他就可以把
    Date d2(2022,9,22)简化成Date d2(d1)那这看起来是不是就方便了很多啊!那我们就称第一种形式为初始化,第二种形式就称为拷贝构造也可以说是拷贝初始化就是那别人的数据来进行初始化,那拷贝构造如何来实现呢?那这里我们就可以接着往下看。

    拷贝构造的特性

    我们首先来看看官方的语言是如何来描述拷贝构造的,拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。大家看了这个描述是不是感到非常的迷茫啊,迷茫就对了但是通过我们上面的讲解我们知道了他的作用,那这里我们就可以来看看拷贝构造的性质性质有哪些,这样我们就能更好的理解这句话,那么拷贝构造有如下几个性质:

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

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

    3. 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
      字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。注意:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

    第一个性质的详解

    第一个性质的内容是:拷贝构造函数是构造函数的一个重载形式。那这句话是不是就告诉了我们拷贝构造函数的外貌跟上面学的构造函数的形式一模一样,就参数的类型,数目和顺序发生了改变,那该函数的参数类型是什么呢?那我们这里就可以这么像想,因为该函数的作用就是将一个值赋值给另外的一个值,所以我们这里顶破天就只有两个参数,而前面我们学过this指针,所以我们这里就只剩下一个参数了,因为我们这个用于的是类之间的赋值,所以该参数的类型就是类,但是还得加个引用和const上去(加const的目的是为了防止我们在赋值的时候将数据写反,如果写反的话就会导致数据没用赋值成功之前的数据还丢失了,加&的原因我们往后再说),那我们这里的拷贝构造函数的轮廓就是这样:在这里插入图片描述
    那我们就拿日期的类来举例子,这就照葫芦画瓢我们的日期类的拷贝构造造函数就如下,当然我们这里就再做一个标记上去这样我们的编译器在调用拷贝构造函数的时候我们能够看的到:

    	Date(const Date& d)
    	{
    		cout <<" Date(const Date & d)" << endl;
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那这里我们就可以去main函数里面使用一下创建几个对象,并用一个对象来初始化另外一个对象,然后看我们这里会不会调用拷贝构造函数那我们的代码如下:

    int main()
    {
    	Date d1(2022, 11, 11);
    	Date d2(d1);
    	d2.print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    运行的结果如下:
    在这里插入图片描述
    那这里我们就可以看到它出现了拷贝构造函数的标志,那这就意味着它调用了该函数,并且调用之后的数据还跟我们的预期一模一样,那这就是我们的第一个特性:拷贝构造函数是构造函数的一个重载形式。

    第二个性质详解

    第二个性质的内容是: 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。那这里我们如何来理解呢?那这个特性最直观的反应就是拷贝构造的参数类型得写成这样:const Date &而不能是这样:const Date 那要搞清楚这里的第二种性质为什么会造成死循环的话,我们首先得看看下面这段代码:

    void func1(const Date d)
    {
    	;
    }
    void func2(const Date& d)
    {
    	;
    }
    int main()
    {
    	Date d1(2022, 11, 11);
    	func2(d1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我们这里调用了第二个函数,这个函数的参数类型是引用,而我们知道当参数是引用的时候他是不会进行所谓的复制过程,并且我们将这个代码运行一下也会发现这里没有什么问题
    在这里插入图片描述
    并且我们的屏幕上面也啥都没有:
    在这里插入图片描述

    但是如果我们这里调用的是第一个函数呢?

    int main()
    {
    	Date d1(2022, 11, 11);
    	func1(d1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们再运行一下:在这里插入图片描述
    这时我们就发现屏幕上面打印出来了一句话,这句话就是调用构造函数的标志,而我们知道func1函数的参数类型是const Date类型,不是引用的类型所以这里在传参的过程中就会出现复制的过程,而这里却出现了调用构造函数现象,那这就可以说明这个传参复制的过程是通过拷贝构造函数来实现的,那我们知道了这一点之后就可以回头再来看看之前的那个问题:为什么我们在写拷贝构造函数的时候,得写成引用的类型。那这里我们先假设不是引用的类型,那这里在调用拷贝构造函数的时候,是不是就得先进行传参啊,而这个传参的过程又是得通过拷贝构造函数来实现的,所以在传参的过程中又会调用拷贝构造函数,而构造函数又得进行传参,所以就又得调用拷贝构造函数等等一直往下循环,所以当我们这里不是引用的话这里就会出现死循环的过程,大家可以通过下面的图片来进行一下理解:
    在这里插入图片描述
    而当我们写成引用的形式的话,就不会出现自己不停循环调用自己的情况,因为它压根就不会调用拷贝构造函数,而是直接使用现成的,那这个特性希望大家能够理解,那这里我们也可以尝试一下将上面的引用去掉再运行一下看看编译器会报出什么样的错误:
    在这里插入图片描述
    那这里的错误太多了,就不一一介绍了,把这里的类型改成引用就可以解决这里的所有问题。

    第三个性质的详解

    第三个性质的内容是:若未显式定义,编译器会生成默认的拷贝构造函数。那这里我们也可以很好的理解,因为拷贝构造函数也是我们的默认成员函数之一,所以他也应该有编译器自动生成的性质,那我们这里就可以用上面的类来测试一下,我们将自己写的拷贝构造函数删除,再在main函数里面使用拷贝初始化看看会有什么样的结果,那我们这里的代码如下:

    class Date
    {
    public:
    	Date(int year, int month, int day)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    
    private:
    
    	int _year=10;
    	int _month=10;
    	int _day=10;
    };
    int main()
    {
    	Date d1(2022, 11, 11);
    	Date d2(d1);
    	d2.print();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    我们将这个代码运行一下:
    在这里插入图片描述
    我们发现这里没用写拷贝构造函数,但是编译器自己生成的拷贝构造函数却依然可以达到我们这里想要的结果,那这是不是就说明了,以后所有的类的拷贝构造函数我们都不用写了吗?答案肯定是不对的,我们再来看看下面的代码,这里我们将类改成Stack的类型并且自己不写拷贝构造函数让编译器自己生成,然后在main函数里面继续使用拷贝初始化看看会发生什么,那我们这里的代码就是这样:

    class Stack
    {
    public:
    	Stack(int capacity =4 )
    	{
    		cout << "Stack(int capacity )" << endl;
    
    		_a = (int*)malloc(sizeof(int)*capacity);
    		if (_a == nullptr)
    		{
    			perror("malloc fail");
    			exit(-1);
    		}
    
    		_top = 0;
    		_capacity = capacity;
    	}
    	void print()
    	{
    		printf("%p\n", _a);
    		printf("%d\n", _top);
    		printf("%d\n", _capacity);
    	}
    	~Stack()
    	{
    		cout << "~Stack()" << endl;
    
    		free(_a);
    		_a = nullptr;
    		_top = _capacity = 0;
    	}
    private:
    	int* _a  ;
    	int _top ;
    	int _capacity ;
    };
    int main()
    {
    	Stack s1(8);
    	Stack s2(s1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    我们来看看这个代码的运行结果:
    在这里插入图片描述
    我们发现这里运行的结果竟然报错了,这是为什么呢?那要想知道这里报错的原因的话,我们就得知道这里编译器自己生成的拷贝构造函数的原理:编译器自动生成的拷贝构造函数他是按照字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。也就说他的拷贝是在内存中一个字节一个字节的拷贝,在我们Date的类中通过这种方式是可以达到我们的需求的,而在Stack的类中就不行因为一个字节一个字节的拷贝就会导致这里s2中的指针指向的空间和s1中的指针指向的空间一模一样,我们这里可以通过调试来看看:
    在这里插入图片描述
    我们可以看到当我们的s2完成了拷贝构造之后他们两的指针都会指向同一块空间,而这就会导致一个问题就是我们的对象在生命周期结束的时候就会调用析构函数,s1会调用一次,s2又会调用一次,而这个析构函数的操作就是这样:

    	~Stack()
    	{
    		cout << "~Stack()" << endl;
    
    		free(_a);
    		_a = nullptr;
    		_top = _capacity = 0;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    他会对_a指针指向的空间进行释放,而s1和s2指向的是同一块空间所以我们这里在释放的时候就会释放两次,所以这就会导致我们的编译器报错,所以面对这个类我们的编译器自动生成的拷贝构造函数就没用达到我们的要求所以我们就只能自己写一个拷贝构造函数上去,那这里怎么写呢?我们上面报错的原因就是因为自动生成的拷贝构造函数是一个字节一个字节的原封不动的拷贝,这种拷贝是浅拷贝,它拷贝的结果就是将s1中的指针指向的地址复制给了s2中的指针,他并没用创建一块新的空间然后将s1中的指针指向的内容复制到s2中的那块新的空间里面去,所以我们自己在写的拷贝构造函数的时候就得做到深拷贝,也就是再开辟一个空间,将s1指针里面的内容复制到新开辟的空间里面去,再将该空间的地址赋值给s2中的指针,那这样的话我们的拷贝构造的代码就是这样:

    	Stack(const Stack& st)
    	{
    		cout << "Stack(const Stack& st)" << endl;	
    		_a = (int*)malloc(sizeof(int)*st._capacity);
    		if (_a == nullptr)
    		{
    			perror("malloc fail");
    			exit(-1);
    		}
    			memcpy(_a, st._a, sizeof(int)*st._top);
    			_top = st._top;
    			_capacity = st._capacity;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    好Stack的拷贝构造函数我们写完了,那这里就有一个问题:MyQueue需要我们自己写拷贝构造函数吗?那为了解决这个问题,我们是可以使用调试的方法来观察一些特殊的值来看是否需要的,那我们这里的代码就如下:

    int main()
    {
    	MyQueue q1;
    	MyQueue q2(q1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们这里依然是使用拷贝初始化的方式来创建我们的q2,那在调试之前我们先来做一下推论:如果q1中的Stack类中的指针指向的地址和q2的那个是一样的,那么这就说明该过程是浅拷贝,没用达到我们的需求得我们自己来写一个拷贝构造函数,如果指针指向的地址不一样则说明这里是一个深拷贝的过程我们编译器自己生成的能够达到我们的要求,那我们这里调试的结果如下:
    在这里插入图片描述
    那么我们这里仔细地观察一下就可以看到q2中的_pushST的指针_a指向的地址和q1中的_pushST的指针_a指向的地址不一样,并且q2中的_popST的指针_a指向的地址和q1中的_pophST的指针_a指向的地址也不一样,那这就说明编译器自己生成的默认构造能够做到深拷贝,能够达到我们的需求所以我们不用在MyQueue的类中写拷贝构造函数,那这是为什么呢?答案也非常的简单编译器自己生成的拷贝构造函数它会对内置类型进行处理,这个处理的形式就是将两个变量的值进行一个字节一个字节的进行拷贝,而对于那种自定义类型编译器自动生成的拷贝构造函数也会做处理,这个处理的方式就调用该自定义类型的内部拷贝构造函数来进行拷贝初始化,那么这就是我们的第三个性质,希望大家能够理解。

    什么时候得自己写拷贝构造函数

    看到了这里想必大家都能够很好的理解拷贝构造函数是什么它有什么样的性质,那这里我们就来讨论最后一个问题:什么时候我们得自己写拷贝构造函数?我们直接来看结论吧:需要写析构函数的类都需要自己写深拷贝的拷贝构造函数,不需要写析构函数的类则不用自己写拷贝构造函数编译器自动生成的就可以了。好!知道结论了我们这里就来想想这是为什么?首先来想为什么我们要写析构函数?这个问题就非常的简单因为当我们对象的生命周期结束的时候,该对象申请的空间喝资源却没有被系统回收,所以我们这里得专门的写一个析构函数来实现回收的作用,那面对这些需要写析构函数的类我们为什么要写拷贝构造函数呢?是不是就是因为编译器自己生成的拷贝构造函数没有将这些空间再创建一份再复制一份的功能啊,所以我们得自己再手动写一个啊,那这是不是就更进一步的说明了,如果你编译器自动生成的析构函数缺乏能够自动回收一些空间或者资源的能力的话,那么他也就一定缺乏能够将这些空间自动创建并且复制一份的能力,所以当我们写析构函数的就得自己手动的写拷贝构造函数。

    哪些场景会用到拷贝构造函数

    这里有三个场景会用到拷贝构造函数:
    1.使用已存在对象创建新对象
    2.函数参数类型为类类型对象
    3.函数返回值类型为类类型对象

    运算符重载函数

    为什么会有运算符重载

    如果说我们想要比较两个整型的数据的大小,那么我们就可以使用c++中的关系操作符来实现比如说:10>9,如果说我们想要得到两个数据的差,那么我们可以使用算术操作符来进行实现比如说:10-9=1,但是我们发现这些操作符所能够面向的对想法都是一些内置内省,比如说整型浮点型等等,那如果我们想要比较两个日期的大小呢?我们想要比较2022年11月11号和2022年12月12号两个日期哪个大,那这里还能够使用我们上面所谓的关系操作符吗?答案很显然是不行的,上面的操作符就完全排不上用场了,那这里我们想要实现比较两个日期的大小的比较话我们就只能通过函数来实现,但是通过函数来实现这个功能的话我们的可读性性就大大的降低了,因为我们人人都知道这些“>” “<” "+"符号的意义是什么,但是你要是写一个函数上去很多小伙伴们可能都反应不过来,所以我们的c++为了解决这个问题,他就给出来了运算符重载这个概念,他可以帮助我们大大的提高代码的可读性,比如说比较两个日期的大小就可以写成这样:d1 > d2 算两个日期中相差的天数我们就可以写成这样:d1-d2这么看的话我们代码的可读性就大大的提高了,那么我们这里是如何来实现运算符的重载的呢?我们是如何做到将一个函数变成一个操作符的呢?那么带着这些问题我们接着往下看。

    运算符重载的形式

    C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator后面接需要重载的运算符符号。那该函数的形式就为:
    在这里插入图片描述
    那我们这里知道了运算符重载的形式,我们就来实操一下看看会遇到什么样的问题,这里就来一个简单一点的,我们如何来实现判断两个日期的是否相等(==)的运算符重载,那我们这里就照葫芦画瓢,首先这个重载之后的返回值是什么?要么相等要么就是不想等,所以我们这里就是bool类型,然后在返回值的类型后面加个operator 再加上我们要重载的操作符也就是==,因为这里终究还是一个函数所以我们这里就得在后面加个括号,在括号里面写入我们的参数,我们这里要比较的是两个日期的类,所以这里的参数就是两个日期类的引用,因为这里不会改变两个日期类的数据,所以我们还得在参数的前面加个const以免发生修改,那么到这里我们的代码就是这样的:

    bool operator ==(const Date& d1, const Date& d2)
    {
    
    }
    
    • 1
    • 2
    • 3
    • 4

    接下来要做的就是完成这个函数体里面的内容,那这就狠好搞嘛我们要判断两个日期是否相等,那直接将这个类中的三个数据一一进行一下比较不就够了嘛,如果年相等,月相等,日也相等的话那我们这里就可以判定两个日期类是相等的,因为我们这里使用的是并且的语句,所以这里就可以使用&&操作符来实现上述的目的,那么我们的代码就如下:

    bool operator ==(const Date& d1, const Date& d2)
    {
    	return d1._year == d2._year
    		&& d1._month == d2._month
    		&& d1._day == d2._day;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    既然我们这里实现了,那我们这里就可以来测试一下看看我们的代码是否正确,那我们测试的代码就如下:

    int main()
    {
    	Date d1(2022, 11, 11);
    	Date d2(2022, 12, 12);
    	cout << (d1 == d2) << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里大家就要注意的一点就是:cout后面的流插入运算符的优先级要高于该==运算符,所以我们要想打印出来这里的结果的话我们就得加一个括号,那么我们将代码运行一下来看看这里会出现什么样的情况:
    在这里插入图片描述
    这是什么情况,为什么我们这里会报出这么多的错误呢?那大家别看这里报了十分多的错误,其实根本的错误就一个就是我们在类的外面访问了一个私有的数据所以报错了,我们来看看代码的截图:
    在这里插入图片描述
    我们这里将运算符重载的代码定义到了全局区域里面,所以我们这里就不能访问私有的数据,那这该如何来修改呢?有小伙伴就说啊,我们将这里的private改成public不就可以了嘛,那我们尝试一下并且再运行一下就会发现我们的运行结果就成了这样:

    在这里插入图片描述
    他这里没有报错然后打印了一个0出来,那这个0表示的意思就是这两个日期的类是不相等的,而我们当时初始化的时候也确实是初始化的不同的数据,那这就说明我们的代码实现的是真确的,但是我们这里的问题并没有解决,因为这么一个小小的缺陷而将我们的所有数据全部都变成public这是得不偿失的,他可能会照成更多的影响,那我们这里就得采用其他的方法来解决这个问题:
    第一个:我们在类中写得到函数
    java喜欢使用这种方法来解决上述的问题,就是在类中写一些函数,这些函数的作用就是得到我们私用数据中的数据,比如说Date类中的年是私有的,所以我们就可以写一个函数该函数的作用就是得到年的数据,那该函数的代码就是这样:

    	int get_year()
    	{
    		return _year;
    	}
    
    • 1
    • 2
    • 3
    • 4

    这里的月也是私有的,所以我们这里也可以写个得到月的数据的函数,那我们的代码就是这样:

    	int get_month()
    	{
    		return _month;
    	}
    
    • 1
    • 2
    • 3
    • 4

    那剩下的数据我们就可以依次类推,这是一种解决方法。

    第二种解决方法:将该函数写入类里面。
    既然在全局区域不能够访问类里面的私有数据,那么我们这里就可以将该函数放到该类里面这样我们就可以访问到该类的私有数据了,那该类的代码就是这样:

    class Date
    {
    public:
    	Date(int year, int month, int day)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    	int get_year()
    	{
    		return _year;
    	}
    	int get_month()
    	{
    		return _month;
    	}
    	void print()
    	{
    		printf("year->%d\n", _year);
    		printf("month->%d\n", _month);
    		printf("day->%d\n", _day);
    	}
    	bool operator ==(const Date& d1, const Date& d2)
    	{
    		return d1._year == d2._year
    			&& d1._month == d2._month
    			&& d1._day == d2._day;
    	}
    
    private:
    
    	int _year=10;
    	int _month=10;
    	int _day=10;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    我们这里再运行一下看看这种解决的方法如何:
    在这里插入图片描述
    哎,奇怪了我们这里为什么也会报出这么多的错误啊,那这里我们就得仔细地来看看这里报错的细节是什么,其中有个错误点说的是此运算符函数的参数太多了,这句话好像可以值得我们来思考一下,我们都知道该操作符(==)的对象只能有两个,但是我们这里写的也确实是两个参数啊?怎么就会多了呢?如果大家是这么想的话,那么就说明大家忘了一个知识点就是在类里面的函数都会有个隐藏的参数就是this指针,因为this指针的存在所以我们这里看似两个的参数实际上有三个,而==该操作数的对象只能有两个,所以这就是我们报错的原因,那这里解决的方法就是得保留第二个参数用this执政来代替第一个参数,那这时我们的代码就成了这样:

    	bool operator ==( const Date& d2)
    	{
    		return this->_year == d2._year
    			&& this->_month == d2._month
    			&& this->_day == d2._day;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们再来运行一下就可以发现我们的编译器没有报任何的问题,并且运行的结果也是正常的:
    在这里插入图片描述
    我们这里的结果跟上面的一模一样,但是有些小伙伴们可能还是会有点疑问就是为什么得保留第二个,而不是第一个呢?那要想解决这个问题的话我们就得先来了解了解operator这个重载的调用形式,我们自己在全局的区域中写了一个运算符重载的函数,然后在main函数中使用这个运算符的时候,其实编译器是会做出一些改变的,比如说下面的代码:

    int main()
    {
    	Date d1(2022, 11, 11);
    	Date d2(2022, 12, 12);
    	d1 == d2;
    	operator==(d1, d2);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们这里用两种方式来调用这里的运算符重载,一种是d1 == d2另外一种就是operator==(d1, d2)
    这两种的调用是没有任何区别的,但是当我们用第一种方式来调用运算符重载的时候我们的编译器其实会自主的将其修改成第二种方式来进行调用,那这就是问题的关键,我们这里是在全局中重载的该运算符,那如果我们是在类里面实现重载该运算符呢?那第二种调用的方式是不是就得做出更改啊,那如何更改呢?c++规定左边=数据为调用的类,右边的数据就变为类中函数的参数,而类中函数的调用的形式为类名+.+函数名+(),所以我们这里调用的形式就变为了这样:d1.ooperator(d2),那这里就可以很好的解决我们上面的问题,他是把右边的数据作为参数传递给我们的重载函数,那看到这里我们就曾热打铁再来实现几个运算符的重载。

    >和>=的运算符重载

    有了上面的基础我们就可以来看看如何实现>的运算符的重载,那这里我们依然争对的是日期的类来实现的,这个重载的作用就是判断左边的日期是否比右边的日期大,那这个重载实现的思路我们就可以通过else if语句来实现,我们首先来判断左边的年是否大于右边的年如果大于的话我们这里就可以直接返回true:

    	bool operator >(const Date& d2)
    	{
    		if (this->_year > d2._year)
    		{
    			return true;
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果我们这里不大于我们就进入下一个else if语句,那这个语句判定的内容就是当左边的year和右边的year相等的时候看左边的月是否等于右边的月,如果是的那我们这里就返回一个true,那我们的代码就如下:

    	bool operator >(const Date& d2)
    	{
    		if (this->_year > d2._year)
    		{
    			return true;
    		}
    		else if (this->_year == d2._year && this->_month > d2._month)
    		{
    			return true;
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    那么跟着这个逻辑往下看的话接下来饿else if语句就是当左边的年合月都和右边的年和月相等的时候如果左边的日比右边的日大的话我们就返回一个true,那如果上面的所有情况我们都没有满足的话我们这里就返回一个false 那我们的代码就是这样:

    bool operator >(const Date& d2)
    	{
    		if (this->_year > d2._year)
    		{
    			return true;
    		}
    		else if (this->_year == d2._year && this->_month > d2._month)
    		{
    			return true;
    		}
    		else if (this->_year == d2._year && this->_month == d2._month && this->_day >d2._day)
    		{
    			return true;
    		}
    		return false}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    那么到了这里我们的代码就写完了,我们就可以用下面的代码来进行一下测试看写的是否是真确的:

    int main()
    {
    	Date d1(2022, 11, 11);
    	Date d2(2022, 12, 12);
    	cout << (d1 > d2) << endl;
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们将这个代码运行一下看看结果如何:
    在这里插入图片描述
    那么这里打印出来的结果是0,这就说明我们的代码写的逻辑是对的,那我们接着来看这个问题>=又如何来写呢?有些小伙伴说我们再根据上面的逻辑重新修改一下不就够了吗?那么这个思路当然没有问题,但是我们这里要学会充分的利用我们已有的资源,我们上面写了等于和大于的运算符重载,而>=不就是>和=的结合吗?所以我们这里就可以直接将代码写成这样:

    bool operator>=(const Date& d2)
    	{
    		return *this > d2 || *this == d2;
    	}
    
    • 1
    • 2
    • 3
    • 4

    +和+=的运算符的重载

    那我们这个运算符重载所想要得到的功能就是得到一个日期后面多少多少天之后的日期,比如说今天是2022年11月11日,那么该日期加上10天之后的日期就是2022年11月21日,那么我们的+就想要达到这样的功能,那我们实现这个功能的思路就是先把天全部都加到日的数据上面,因为我们每个月能够容纳的天数都是确定的,所以当日的天数大于该月能够容纳的天数之后我们就把当前日的天数减去该月所对应的天数,然后再把月的数据加上一,比如说求2022年的11月11号+30,那么我们的第一步就是把30加到日上就变成了2022年11月41日,因为我们的11月只有30天而41大于30,所以我们就让41减去30得到了11,然后再让月的数据加上1,也就是11+1=12,那么这里大家要注意的一点就是我们的月的数据是不能超过12的,所以当月的数据大于12的时候我们就得将月的数据置为1,然后让年的数据加上1,因为年没有限制所以我们这里就不需要管,那么这就是我们的大致思路,我们的第一步就是写一个得到每个月的天数的函数,那么这个函数就非常的简单,年有闰年和非闰年之分,而这个区别就直接的影响了2月的天数,所以我们这里的代码就是这样:

    	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];
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    首先我们将天的数目加到日的数据上面:

    	Date& operator +(int day)
    	{
    		this->_day += day;
    	}
    
    • 1
    • 2
    • 3
    • 4

    然后我们再创建一个while循环,该循环执行的条件就是当日数据大于该月对应的天数时我们就执行该循环,在循环体里面先让日的数据减去该月对应的天数,然后再让月的数据加一:

    	Date& operator +(int day)
    	{
    		this->_day += day;
    		while (_day > GetMonthDay(this->_year, this->_month))
    		{
    			this->_day -= GetMonthDay(this->_year, this->_month);
    			this->_month += 1;
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    然后我们再在里面加入一个if语句,当月的数据大于12的时候我们就让年的数据加上1,让月的数据重新等于1,等我们的循环结束之后我们就返回*this:

    	Date& operator +(int day)
    	{
    		this->_day += day;
    		while (_day > GetMonthDay(this->_year, this->_month))
    		{
    			this->_day -= GetMonthDay(this->_year, this->_month);
    			this->_month += 1;
    			if (this->_month > 12)
    			{
    				this->_year += 1;
    				this->_month = 1;
    			}
    		}
    		return *this;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    那么看到这里我们的代码就结束了,但是大家有没有发现一个问题就是,我们的+他应该时不会改变这里本省的数据的,比如说一个变量a的值加5,那这个a本身的值他是不会发生改变的,而我们上面的操作他是在不停的改变的对象本身的数据,所以按理来说我们上面并不是+的运算符重载而是+=的运算符重载,所以我们这里得进行一下修改将operator后面的+改成+=,那么如果想要实现+的运算符重载呢?那我们该如何进行修改呢?那么我们这里就可以首先将数据复制一份,然后对复制出来的对象执行一下加等操作不就可以了嘛,最后再把复制出来的对象返回,那么这里大家有一点要注意的就是,我们上面的返回是引用返回,而这里采用的返回得是值返回,因为当出了该函数的时候,复制出来的对象的生命周期就已经结束了,而对于这种情况我们是不能采用引用返回的,所以就只能采用值返回,那么我们修改后的代码就是这样:

    	Date operator +(int day)
    	{
    		Date d1(*this);
    		d1+=day;
    		return d1;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们这里可以对两个操作进行一下测试,看看我们写的对不对:

    int main()
    {
    	Date d1(2022, 11, 11);
    	(d1 + 10000).print();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们来看看2022年11月11日后的一万天后的日期是多少:

    在这里插入图片描述
    2050年3月29号,我们可以在浏览器上搜索一下看看这个计算的结果是不是对的,
    在这里插入图片描述
    我们写的代码跟浏览器上算的一模一样,所以我们算的应该就是对的,那我们接着往下看看其他运算符的重载。

    -和-=的运算符重载

    既然我们能够算一个日期对应的多少天之后的日期,那么同样的道理,我们这里也可以算一个日期对应的多少天之前的日期,比如说2022年11月11号对应的10天前的日期就应该是2022年11月1号,那么这里的运算公式就是2022年11月11日-10,所以这里就是这两个运算符重载对应的功能,那这里-=对应的重载实现的思路也就非常的简单,在+=的重载实现过程中,我们是让天数加到日的数据上面去,然后不停的判断当前日的数据是否比月对应的天数要大并依次来判断循环是否要继续,那对应着这里的-我们就来执行着相反的操作,我们先让当前日的数据减去当前的天数,然后再在一个循环里面判断当前日的数据是否大于0,如果不大于0的话我们就让月的数据减去1,然后再把当前月对应的天数加到日的数据上面去,那么这里我们的代码就是这样:

    	Date& operator -=(int day)
    	{
    		this->_day -= day;
    		while (this->_day < 0)
    		{
    			this->_month -= 1;
    			this->_day+= GetMonthDay(this->_year, this->_month);
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    因为月他是不能够等于0的,所以每当月等于0的时候我们就将月的数据置为12,最后再返回*this那么我们的代码如下,:

    	Date& operator -=(int day)
    	{
    		this->_day -= day;
    		while (this->_day < 0)
    		{
    			this->_month -= 1;
    			if (this->_month == 0)
    			{
    				this->_month = 12;
    				this->_year -= 1;
    			}
    			this->_day+= GetMonthDay(this->_year, this->_month);
    		}
    		return *this;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    既然我们的-=实现了,那么-也就非常的好实现,还是跟上面一样的套路,先复制一份出来然后对复制出来的对象执行-=操作,再返回该复制出来的对象,那么我们这里的代码就是这样的:

    	Date& operator-(int day)
    	{
    		Date d1(*this);
    		d1 -= day;
    		return d1;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们可以写代码来测试一下看我们这里的实现是否真确,那么代码如下:

    int main()
    {
    	Date d1(2022, 11, 11);
    	(d1 - 10000).print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    运算的结果就如下
    在这里插入图片描述
    我们再来看看浏览器中中算的结果是什么:
    在这里插入图片描述
    那么我们的代码运算的结果就和浏览器中计算的结果是一模一样,那这就说明我们这里的代码实现的逻辑是对的。但是我们这里还是存在的一个问题就是,我们这里输入的都是正整数,那如果输入的是一个负数,那我们这里的逻辑还是对的吗?我们可以来看看将这里的测试代码中的参数改成负数看看这里的运行结果还是对的吗?

    int main()
    {
    	Date d1(2022, 11, 11);
    	(d1 - (-10000)).print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们将其运行一下看看这里的结果如何:
    在这里插入图片描述
    我们发现这里的运行结果就很明显是错误的,那我们这里如何来进行修改呢?我们这里计算的是多少天之前的日期,如果我们这里输入是负数的话,那对应的应该是多少天之后的日期,所以我们这里就可以在里面加一个if语句,当该函数接收的参数是负数的话,我们就可以在里面将参数改为正数,然后使用+的相关运算符重载来得到想要的结果,那我们这里的代码如下:

    Date& operator -=(int day)
    	{
    		if (day < 0)
    		{
    			*this += (-day);
    			return *this;
    		}
    		this->_day -= day;
    		while (this->_day < 0)
    		{
    			this->_month -= 1;
    			if (this->_month == 0)
    			{
    				this->_month = 12;
    				this->_year -= 1;
    			}
    			this->_day+= GetMonthDay(this->_year, this->_month);
    		}
    		return *this;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    我们再将上面的测试代码运行一下就可以看到我们这里的结果就是真确的:
    在这里插入图片描述
    跟浏览器的运算结果一模一样:
    在这里插入图片描述
    那同样的道理,在+的运算符重载里面我们也可能会出现负数的情况,当我们计算往后的一个负数的天数的话,那不就是计算的不就是之前的日期了吗?所以我们这里就在+的函数里面加个if语句,在里面使用-有关的预算符来实现这种情况,那我们这里的代码就是这样:

    Date& operator +=(int day)
    	{
    		if (day < 0)
    		{
    			*this -= (-day);
    			return *this;
    		}
    		this->_day += day;
    		while (_day > GetMonthDay(this->_year, this->_month))
    		{
    			this->_day -= GetMonthDay(this->_year, this->_month);
    			this->_month += 1;
    			if (this->_month > 12)
    			{
    				this->_year += 1;
    				this->_month = 1;
    			}
    		}
    		return *this;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    我们写个代码测试一下来看看这里运算的结果是否是真确的:

    int main()
    {
    	Date d1(2022, 11, 11);
    	(d1 + (-10000)).print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    代码的运行结果如下:
    在这里插入图片描述
    浏览器运算的结果如下:
    在这里插入图片描述
    那么这就说明我们的代码运行的结果是正确的,那我们接着往下看学习另外一个内容。那看到这里大家肯定能够想到-的另外一个重载的功能,就是计算两个日期相差多少天的时候我们就可以使用减这个运算符重载来进行表示,比如说2022年11月11日-2022年11月1日就等于10天,2022年11月1日-2022年11月11日就等于-10天,那这种功能我们的运算符重载如何来实现呢?那我们这里的思路就十分的简单粗暴,首先我们得知道你给的两个日期哪个大哪个小,然后如果左边的日期大的话我们返回的结果就得是个正数,如果左边的日期较小的话那么你返回的结果就得是个负数,那么我们这里就可以先创建两个日期的对象分别叫做Max和Min,然后把*this赋值给Max,将d1赋值给Min,再创建一个标志性的变量flag,并将它的值初始化为1,那么这个标志变量的作用就是判断最后返回的值是正数还是负数,再用if语句来进行一个判断,当d1大于*this的时候,我们就把Max和Min的值进行一下互换,并将flag的值变为-1,因为当d1大于*\this的时候,它返回的值是一个负数,所以我们最后得用flag来乘以最后的结果以表示负数,那我们这里的代码就是这样:

    	int operator-( Date& d1)
    	{
    		Date Max(*this);
    		Date Min(d1);
    		int flag = 1;
    		if (d1 > *this)
    		{
    			Max = d1;
    			Min = *this;
    			flag = -1;
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    然后我们这里要干的一件事情就是创建一个循环,这个循环结束的条件就是当Max和Min相等的时候就结束这个循环,然后在循环里面我们创建一个变量num来记录一下天数,每次循环我们都让num的值加1,并且还让Min的值加一,这样当循环结束的时候我们就可以知道了中间相差的天数是多少,最后将flag和num的乘积最为返回值来结束这个函数,那我们的代码就如下:

    	int operator-( Date& d1)
    	{
    		Date Max(*this);
    		Date Min(d1);
    		int flag = 1;
    		if (d1 > *this)
    		{
    			Max = d1;//两个类的赋值我们后面再讲
    			Min = *this;
    			flag = -1;
    		}
    		int num = 0;
    		while (Max > Min)
    		{
    			num++;
    			Min += 1;
    		}
    		return flag * num;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    我们可以用下面的代码来测试一下看看我们这里的代码写的是否是真确的:

    int main()
    {
    	Date d1(2022, 11, 11);
    	Date d2(1700, 1, 22);
    	cout << (d1 - d2)<<endl;
    	cout << (d2 - d1) << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们来看看我们的编译器运行的结果:
    在这里插入图片描述

    我们来看看浏览器计算的结果是否和我们的一样:
    在这里插入图片描述
    那这里计算的结果和我们的代码算的结果一模一样,所以我们这里的代码实现就是真确的。

    运算符重载的一些性质

    性质一
    不能通过连接其他符号来创建新的操作符:比如operator@,我们可以看看编译器遇到这种错误会报出什么样的警告:

    	void operator@(void)
    	{
    		;
    	}
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述
    那么将这里的代码运行一下就可以看到这里报出了各种奇奇怪怪的错误,这些错误的原因就是因为我们这里像重载一个新的操作符,那么这个肯定是不允许的。
    性质二
    重载操作符必须有一个类类型参数。比如说我们下面的代码:

    bool operator+(int a, int b)
    {
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4

    我们在全局区域写的一个预算符重载的代码,我们将其运行一下来看看编译器报出什么样的错误:
    在这里插入图片描述
    大家可以看到,这里编译器也说了当我们使用operator来实现重载的时候必须得有一个类类型的参数。
    性质三
    作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。
    性质四
    “.*” “::” " sizeof " " ? : " " . " 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。

    赋值重载的介绍

    赋值重载那顾名思义就是将操作符" = "进行重载,那么这里重载之后所能够达到的功能就是将一个类的数据赋值给另外一个类,并且赋值重载他还是一个默认成员函数,那这有小伙伴可能就有疑问了,我们的拷贝构造函数不也是这个功能吗?为什么要在这里搞个赋值重载呢?那么这里大家要搞清楚的一件事就是拷贝构造针对的是一个马上就要创建出来的对象,而这里的赋值重载针对的是已经存在的两个对象,比如说我们下面的两段代码:

    int main()
    {
    	Date d1(2022, 11, 22);
    	Date d2(d1);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这个就是拷贝构造,当d2真正创建的时候将d1的值赋值给d2

    int main()
    {
    	Date d1(2022, 11, 22);
    	Date d2(2022, 12, 12);
    	d1 = d2;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这个就是赋值重载在使用之前,d1和d2已经存在了那么这就是两者最主要的区别。

    赋值重载的例子

    我们这里先实现Date类里面的赋值重载,因为=也是运算符所以我们这里的格式跟上面的差不多,那么这里的代码就如下:

    	void operator=(const Date& d1)
    	{
    		this->_day = d1._day;
    		this->_month = d1._month;
    		this->_year = d1._year;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    因为在该函数里面我们是不会改变参数里面的值的,所以我们在参数的前面加个const来以防值被修改,因为以引用的形式传参可以减少我们性能的消耗,所以我们这里就加个引用的标识符上去也就是这个&,因为我们这里就是一个赋值的过程没用什么要返回的内容,所以该函数的返回的类型就是一个void,那么我们这里可以用下面的代码来测试一下看我们这里实现的思路是否是真确的:

    int main()
    {
    	Date d1(2022, 11, 22);
    	Date d2(2022, 12, 12);
    	d1 = d2;
    	d1.print();
    	d2.print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    那么该代码的运行结果如下:

    在这里插入图片描述
    那么我们这里一开始初始化的值是不一样的,但是经过我们的赋值操作符的作用之后,他们打印出来的值变得一模一样,那么这就说明我们的运算符重载实现的大致是真确,但是这里还有一个问题没用解决就是:我们的赋值操作符它是可以实现连续赋值的比如说我们下面的代码:

    int main()
    {
    	int a = 10;
    	int b = 5;
    	int c = 7;
    	a = b = c;
    	printf("a = %d\n", a);
    	printf("b = %d\n", b);
    	printf("c = %d\n", c);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们将这段代码运行一下就可以看到我们这里打印的结果全部都是7:
    在这里插入图片描述
    但是我们上面实现的那个赋值重载它可以达到这样的功能吗?我们可以再写一个测试代码来看看:

    int main()
    {
    	Date d1(2022, 11, 22);
    	Date d2(2022, 12, 12);
    	Date d3(2022, 11, 11);
    	d1 = d2= d3;
    	d1.print();
    	d2.print();
    	d3.print();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们将这个代码运行一下就可以看到这里报出了一些错误:
    在这里插入图片描述
    那这里报错的原因就是因为我们上面在实现的过程中没有给这个函数赋予一个返回值,我们把这个函数的返回值设定为void,而实际上a=b这个运算的过程他是有返回值的,我们可以用下面的代码来验证一下:

    int main()
    {
    	int a = 3;
    	int b = 7;
    	printf("%d", a = b);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    将其运行一下就可以看到这里打印出来的结果就是这个操作符右边的值:
    在这里插入图片描述
    那我们这里如何来进行修改呢?因为经过赋值之后操作符的右边和左边的值都是一样的,所以我们这里可以将*this作为该函数的返回值,并且随着函数的结束*this依然是存在的,所以我们这里的就可以采用引用返回的形式,那么我们这里修改之后的代码就是这样:

    	Date& operator=(const Date& d1)
    	{
    		this->_day = d1._day;
    		this->_month = d1._month;
    		this->_year = d1._year;
    		return *this;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们再来看看上面的代码是否能够运行正确:
    在这里插入图片描述
    那么经过上面的修改我们这里的运行的结果就是正确确的。我们说赋值重载也是一个函数默认成员函数,所以当我们自己没有写赋值重载函数的时候,编译器自己也会生成一个,而且它自己生成的赋值重载函数的性质跟自动生成的拷贝构造函数的性质差不多是一样的也就是浅拷贝,对于内置类型以值的方式逐字节拷贝,而自定义类型成员变量则需要调用对应类的赋值运算符重载来完成赋值。比如说我们下面的代码:

    int main()
    {
    	Stack s1(8);
    	Stack s2(10);
    	s1 = s2;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们这里没用在该类中写对应的赋值重载函数,所以这里的编译器会自动地生成一个,但是我们这里运行一下就可以发现这里报出了错误:
    在这里插入图片描述
    根据之前地经验我们知道这里报错地原因是因为浅拷贝导致了这两个类中的指针全部都指向了同一块空间,而在生命周期结束的时候调用了两次析构函数,所以就会导致同一个空间被释放了两次,那么这就是我们这里报错地原因,但是这里的错误不止有这里的一个,因为我们这里在创建对象的时候会调用两次构造函数,而栈里面的构造函数会创建两个空间,并分别用两个指针指向了这里的空间,所以当用编译器生成的赋值重载函数来进行赋值的时候,它会让两个指针指向同一个空间,而当我们要释放的时候,其中一个空间会被释放两次,而另外一个空间由于没用指针指向它,所以它不会被释放,那么这也就意味还有内存泄漏的问题在里面,所以对于栈这种类我们就得自己写赋值重载函数来做到深拷贝,那我们这里实现的思路就是先把原来的空间进行释放,再在堆上申请一个跟赋值对象一样大的空间,并把对象中的数据全部都拷贝到这个空间里面,那么我们这里代码就是这样:

    	Stack& operator=(const Stack& s)
    	{
    		free(_a);
    		_a = (int*)malloc(sizeof(int) * s._capacity);
    		if (_a == nullptr)
    		{
    			perror("malloc fail");
    			exit(-1);
    		}
    		memcpy(_a, s._a, sizeof(int) * s._top);
    		_top = s._top;
    		_capacity = s._capacity;
    		return *this;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我们再来将这个代码运行一下就可以发现这里的代码并没有报错,并且类中的数据也变得一模一样:
    在这里插入图片描述
    但是写到这里我们得代码一定是真确的吗?我们都知道赋值预算符是可以自己给自己赋值的,比如说下面的代码:

    int main()
    {
    	int a = 10;
    	a = a;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那我们这里实现的赋值重载他也可以这么做吗?我们来看看下面的代码:

    int main()
    {
    	Stack s1;
    	s1.Push(1);
    	s1 = s1;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在执行s1=s1之前我们类中的数据是这样的:
    在这里插入图片描述
    但是我们在执行之后我们类中的数据就变成了这样:
    在这里插入图片描述
    那大家对比一下就可以发现一个很奇怪的事情就是经过我们这里的赋值,该对象中的数据发生了很大的改变变成了随机值,那这是为什么呢?本来应该不发生任何变化的啊,那么这里大家就得回到我们写的赋值重载函数里面,我们首先是释放掉了这里原来的空间,然后再将数据进行拷贝,但是我们这里他是同一个东西啊,你释放了其中一边的空间,那也就意味着=两边的数据都丢失了,而这时你再进行数据的复制,复制不就都是随机值了吗,所以为了解决这个问题我们就写一个if语句,如果this和引用参数的地址不一样的话我们就执行下面的赋值过程,如果一样的话我们就直接返回*this,不做任何的处理,那么我们的代码就是这样:

    	Stack& operator=(const Stack& s)
    	{
    		if (this != &s)
    		{
    			free(_a);
    			_a = (int*)malloc(sizeof(int) * s._capacity);
    			if (_a == nullptr)
    			{
    				perror("malloc fail");
    				exit(-1);
    			}
    			memcpy(_a, s._a, sizeof(int) * s._top);
    			_top = s._top;
    			_capacity = s._capacity;
    			return *this;
    		}
    		return *this;
    
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    我们再来调试一下就可以发现我们这里的问题就解决了数据没用发生改变:
    在这里插入图片描述

    赋值重载的注意事项

    赋值运算符只能重载成类的成员函数不能重载成全局函数。比如说我们下面的代码:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    我们将其运行一下就可以看到这里报出了许多的错误:
    在这里插入图片描述
    那这里报错的原因也非常的简单就是因为原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。在c++ prime中也提到了这个问题:
    在这里插入图片描述
    那么这里还有一个问题就我们什么时候得自己写赋值重载函数: 那这个问题就和拷贝构造函数的性质是一模一样的,当我们要自己写析构函数的时候,我们就得自己写赋值重载函数。

    <<和>>的运算符重载

    我们首先来看看下面的代码

    int main()
    {
    	int i = 10;
    	cout << i << endl;
    	double d = 1.11;
    	cout << d;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们这里创建了两个不同类型的变量,然后用cout将这两个不同的变量打印出来,然后我们运行一下就可以看到我们这里的屏幕确实打印出来了这些变量的值:
    在这里插入图片描述
    那么我们这里能够打印两个不同类型的值,那这就说明该操作符他是可以实现自动识别的功能,而实现该功能的原理还是因为函数重载,也就是我们上面所说的运算符重载,比如说我们想让编译器计算5+3的结果,我们的编译器能够很快的算出来,因为这里的5和3都是内置类型,而且编译器自己也是能够支持+和-的计算的,所以他能够将这个算式变成对应的汇编指令,那如果我们将这里的内置类型改成自定义类型的话,他还能够转换成对应的汇编指令吗?那很显然他是不能的,因为编译器他不知道这里的自定义类型是什么,只有我们创建者知道,所以编译器无法自动的将其转化为对应的汇编指令,所以这时我们创建者要想他能够转化成对应的汇编指令的话,我们就得对这里的加减运算符进行运算符的重载,那我们再来看上面的代码:

    cout<<i;
    cout<<d;
    
    • 1
    • 2

    这里的i和d都是内置类型,而且<<该操作符编译器也能够知道他的功能,但是这里的cout他不是内置类型,他是自定义内省,他不是我们写的也不是编译器自动存在的而是库中写的,在istream里面他就默认支持了各种内置类型,实现了对各种内置类型的<<的预算符重载,这就是为什么这里能够自动识别各种类型的原因,那么我们可以通过下面的图片来看看我们上面说的这些内容:在这里插入图片描述
    那么这里就有一个问题我们的<<能够识别我们的Date类吗?他能够打印这个类中的数据吗?答案很显然是不能的,因为我们的库中没用让<<支持日期的类,所以我们要想让<<支持日期的类,就得我们自己来写,我们可以看看下面的代码:

    int main()
    {
    	Date d1(2022, 11, 11);
    	cout << d1;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们发现<<有两个操作的对象一个是日期类型的对象d1,另外一个ostream类型的cout,所以我们在重载函数里面的声明就可以这么写:

    	void operator<<(ostream out)
    	{
    
    	}
    
    • 1
    • 2
    • 3
    • 4

    然后在函数里面我们就可以通过cout以我们想要的形式来打印这里的数据比如说这样:

    	void operator<<(ostream out)
    	{
    		cout << "year->" << _year << endl;
    		cout << "month->" << _month << endl;
    		cout << "day->" << _day;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那么我们这里就可以用上面的代码来测试一下这里的实现的正确性:

    int main()
    {
    	Date d1(2022, 11, 11);
    	cout << d1;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们运行一下代码看看结果如何:
    在这里插入图片描述
    我们可以看到这里爆出来了很多的错误,那这里子所以会这样的原因就是因为,我们这里传参的顺序发生了错误,cout是<<的左参数而d1是<<的有参数,而我们知道类中自动生成的this指针他永远是在参数中的最左边,所以我们这里是把cout传给了函数中的this指针,把d1传给ostream类型的参数,所以这里就爆出来错误, 因为this指针的位置是无法修改的,所以我们这里就只能将这个运算符的重载放到类的外面,并把参数的顺序进行修改,但是这里还是有个问题就是如果该函数定义在类的外面的话,他如何来访问类中的数据呢?那么为了解决这个问题我们来引入一个新的知识点叫友元,我们在内中写一个这个函数的声明,然后在声明的前面加个friend就可以让这个定义在类外面的函数使用这个类中的私有数据,那么我们的代码就是这样:

    class Date
    {
    public:
    //.
    //.
    //.
    //.
    friend void operator<<(ostream& out, const Date& d);
    private:
    
    	int _year=10;
    	int _month=10;
    	int _day=10;
    };
    void operator<<(ostream& out, const Date& d)
    {
    	cout << "year->" << d._year << endl;
    	cout << "month->" << d._month << endl;
    	cout << "day->" << d._day;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    那么我们这里再运行一下上面的代码来拿看看这里的代码实现的结果如何:
    在这里插入图片描述
    我们可以看到这里我们成功的打印出来了这些数据,但是我们这里的问题却没用完全的解决首先有个问题就是用流插入操作符操作内置类型的时候我们时可以可以链式操作的,比如说下面的代码:

    cout<<b<<c<<d
    
    • 1

    但是我们上面的代码,似乎就没用这个功能,那么我们这里实现的逻辑就是链式结构的执行顺序时从左到右,而每次操作返回的值是左边的对象,也就是这里的cout,所以我们就得对上面的函数做出一些修改,将返回值的void改成ostream&,在函数的结尾加一个return out这样就可以实现上面的功能:

    ostream& operator<<(ostream& out, const Date& d)
    {
    	cout << "year->" << d._year << endl;
    	cout << "month->" << d._month << endl;
    	cout << "day->" << d._day;
    	return out;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    然后这里还有个问题就是,我们这里是将函数定义在.h文件里面,也就是所谓的头文件里面,而像这种头文件一般都会被引入到许多的.cpp文件里面进行使用吗,而我们这里的函数定义的位置是全局变量里面,而多次引入该头文件就会导致我们不停的重定义了该函数,就会报出错误,那么我们这里解决问题的方法就是在函数的前面加上inline或者static让其不进入符号表或者改变其连接性,那么我们的代码就变成了这样:

    inline ostream& operator<<(ostream& out, const Date& d)
    {
    	cout << "year->" << d._year << endl;
    	cout << "month->" << d._month << endl;
    	cout << "day->" << d._day;
    	return out;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么这里大家有了流插入的操作符重载那么流提取也可以依次来照葫芦画瓢,那么代码如下:

    inline istream& operator>>(istream& in, Date& d)
    {
    	in >> d._year >> d._month >> d._day;
    	return in;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ++的运算符重载

    那么我们这里的++能够遇到的最大的问题就是如何来区分前置++和后置++,那么为了解决这个问题我们的c++就规定后置++的运算符重载就加一个参数,比如说我们下面的代码:

    	Date operator ++(int i)
    	{
    		Date tmp(*this);
    		*this += 1;
    		return tmp;
    	}
    	Date operator ++()
    	{
    		*this += 1;
    		return *this;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们这里对前置++和后置++都做了重载,但是这里的第一个有参数的重载是后置++的重载,而第二个没用参数的是前置++的重载,那么我们的编译器在调用的时候就会自动的将++d变成d1.operator ++ ( )将这里的d1++自动的变成d1.operator( 1 ),所以这里的参数就是用来做区分的没有别的作用,大家注意一下就行,有参数的是后置++,没有参数的是前置++。但是这里要注意的一点就是对于自定义类型要避免使用后置++,因为这里会调用两次拷贝构造函数会导致效率的降低,而且这里的++是这样–在实现的时候也是如此。

  • 相关阅读:
    Golang Sync.WaitGroup 使用及原理
    循序渐进了解如何使用JSR303进接口数据校验
    Linux篇 四、Linux修改用户名
    18.2.1 创建分区表
    You have mail in /var/mail/root
    Consul学习笔记之-初识Consul
    postgresSQL多种字符串分割及查询汇总
    Linux内核源码编译-built-in.o 文件编译生成过程
    [交互]实战问题2-413 Request Entity Too Large
    解决 80% 的工作场景?GitHub 爆赞的 Java 高并发与集合框架,太赞了
  • 原文地址:https://blog.csdn.net/qq_68695298/article/details/127925850