引言:
北京时间:2023/2/6/10:45,今天起床时间10:00,睡迟了,可能是因为昨天睡的有点晚,但是庆幸的是昨天没人叫我玩鹅鸭杀,我们把博客给更新了,哈哈哈!所以今天我们趁热打铁,一鼓作气把类和对象给搞定。但是伴随着开学的钟声快要响起,考试即将来临,说不慌是假,但是我就是没有复习的动力,可能是上课太过于摆烂,连复习什么我都不知道,也可能是我觉得小小的考试,60分并难不倒我,也可能是我对C++的学习比较上头。反正综合现在来看,C++对我的吸引比较强。所以车道山前必有路,开学干的事情,开学再说,现在是我自己的快乐时间,我们今天就来把有关类和对象中的重点知识,运算符重载以及相关知识学习一下。
我们在前面学习了构造函数,析构函数,拷贝构造函数以及相关的知识之后,此时我们就利用这些知识来实现一个计算日期的日期类代码。意思为在一个日期上加上天数,然后得到加上天数之后的日期
代码如下:也算是复习和运用之前的各种知识
深入拷贝构造
从上篇博客中,我们知道了什么是拷贝构造,并且大致学会了如何使用拷贝构造,此时我们就再来看看拷贝构造中的一些细节方面。
如下代码:
如图所示,我们可以发现拷贝构造是会直接对内置类型进行初始化的,其实按照昨天的知识,我们知道拷贝构造其实就是一个构造函数,但是它初始化的数据是从别的对象中复制过来的而已。但是拷贝构造和构造函数两者又有一点的不同,因为构造函数只会对自定义类型进行初始化而不会对内置类型进行初始化,但我的拷贝构造函数却不仅会对内置类型进行初始化,也会对自定义类型进行初始化(代码还没有演示),所以拷贝构造和构造函数在某些方面还是有很大的不同的。并且由于我们的拷贝构造函数是会自动对内置类型进行初始化的,所以以后我们在写内置类型的时候,是不需要去自我实现拷贝构造函数的,编译器会自己去调用拷贝构造函数。当然前提是我拷贝的对象已经被构造函数初始化过,这样才可以实现自己不实现拷贝构造,而使我的创建对象被初始化。
接着上面的内容,我们来看一下拷贝构造是如何对自定义类型进行初始化的,如下代码我们会发现一个很重要的问题:
我们发现我们在进行拷贝构造的时候,确实是不需要自己去实现拷贝构造函数的,编译器会自己去调用编译器系统内部的拷贝构造函数,从而实现内容数据的拷贝,但我们此时还会发现,我们的自定义类型中,虽然内容被拷贝了,但此时int* _a指向了一块空间,编译器指向拷贝构造之后,导致st2对象中的int*_a此时也被初始化成指向了st1指向的那块空间(从图中的两个内存地址相同可以看出),所以现在就会有一个致命问题需要我们解决,就是深拷贝问题,并且可以发现,我们的编译器虽然会自动调用拷贝构造,但是该拷贝构造只是一个浅拷贝,编译器没有执行深拷贝的能力,所以当我们遇到自定义类型需要进行拷贝的时候,无论是浅拷贝还是深拷贝,此时编译器是不敢乱自动去调用系统内部的拷贝构造,编译器此时必须优先调用我们自己实现的拷贝构造函数,而不是自动调用编译器系统内部的拷贝构造函数。
从深浅拷贝问题深入拷贝构造
所以如果按照上述的代码,我们没有自己去实现拷贝构造函数,而是让编译器去自动调用系统内部的拷贝构造,那么此时程序最终会崩溃,因为当程序运行到最后是,会去调用我们的析构函数,让析构函数帮我们把内部的空间资源给清理掉,然而此时因为st1和st2都存储在main函数的栈帧上,那么此时main函数栈帧在销毁前,会对st1和st2进行清理,并且按照栈帧原则(先进后出),此时是先对st2这个后定义的对象清理,再对st1这个先定义的对象清理,但st1和st2对象中的int*_a指向了同一块空间,那么在清理是导致st2中int*_a先被清理(清理就是析构函数,释放内存,并置成空指针),重点:虽然st2在清理时,将int*_a指向的空间置成了空指针,但是该空间的变化是针对于st2这个对象空间,此时并不会影响到st1对象的空间,如下图所示,此时就让我们带着这个问题,真正的进入到内存之中去看待深浅拷贝问题,此时我们参考如下代码,从代码方面去看看深浅拷贝和内存之间的关系。通过深浅拷贝问题来解决拷贝构造的问题。
此时当我们程序要结束时(就是执行到了return),我们的编译器会自动调用我们的析构函数,来清理资源,此时我们发现,析构函数是清理我们的st2,并且可以发现,当析构函数执行完之后,我们的int*_a指针确实指向了空,但我们的st1中int*_a指针指向的却并不是空。所以通过这个图,我们确实是可以知道清理st2对象的空间是不会影响到st1的空间的,但是,我们发现我们的程序却依然还是崩溃了,如下图:
这是什么原因呢?为什么我的程序还是会崩溃呢?原因就是:我的st2对象中的int*_a指针指向的空间被释放之后,导致st1中的int*_a指针指向的空间被释放,此时就导致st1中的int*_a指针指向了一块随机的内存空间(就是变成了野指针),所以此时程序就因为野指针问题,导致崩溃了,所以当两个不一样的指针,指向了同一块空间之后,此时是会造成很大的问题的,所以为了避免造成两个指针指向同一块空间的问题,我们的编译器是不允许直接调用系统编译器内部的拷贝构造函数,必须优先调用我们自己实现的拷贝构造函数(只有这样才可以区分深拷贝和浅拷贝问题)。
总:指向同一块空间的问题,插入和删除数据会互相影响,并且导致该空间会析构两次,程序崩溃。所以不允许编译器自动调用系统编译器内部的拷贝构造,而是优先调用自己实现的拷贝构造。
并且拷贝构造的典型使用场景有:
使用已存在对象创建新对象 |
函数参数类型为类类型对象 |
函数返回值类型为类类型对象 |
见见猪跑,看一下到底什么是深拷贝:如下图
这样我们就把深拷贝和拷贝构造直接的一些不间接关系给搞清楚了,但此时我们会有一个疑问,那就是我们什么时候需要自己写拷贝构造,什么时候不要写拷贝构造呢?通过上述知识可能有的同学就会想,只要当我们有指针指向另一块空间的时候就要写拷贝构造,没有的时候就不要,这个是有一定道理的,但是不是绝对的,所以我们一般在我们自己实现了析构函数的时候,我们就需要自己实现一个拷贝构造,这个是很有道理的。
总:当我们自己实现了析构函数的时候,我们就要自己再实现一个拷贝构造,不然就会出大问题。
什么是运算符重载
运算符重载:C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。函数名字为:关键字operator后面跟上需要重载的运算符符号。函数原型:返回值类型operator操作符(参数列表),例:bool operator==(const Date& d1, const Date& d2);
注意:
搞明白了上述的基础知识,此时我们就可以真正的进入到运算符重载的学习了,首先搞定的第一点,我们想到,C语言中都没有什么运算符重载的概念,为什么C++中却有这个运算符重载的概念呢?强调,我们一直秉承着C++就是为了弥补C语言的不足而开创的,所以自然而然运算符重载这个概念的提出就是为了可以补充一些C语言中的不足,所以接下来我们就介绍一下运算符重载的好处和无运算符重载的坏处,同时我们都是以日期类代码来实现的。
如下图:
了解完了我们为什么要引入运算符重载的这个概念,此时我们就深入其内部,深入学习运算符重载和其相关的一些知识,首先我们可以知道运算符重载函数不仅可以放在全局中,也可以放在我们的类中,并且将运算符重载函数,放到了类中之后,此时就如上述注意中的第3点所说,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏this。并且使用运算符重载,就是把运算符重载的函数理解成是一个正常的函数,只是名字比较特殊一点点而已。当我们定义运算符重载函数的时候,运算符的返回值是bool值(真返回1,假返回0),参数由运算符的操作数决定(该运算符有几个操作数就有几个参数),并且第一个参数就是左操作数,第二个参数就是右操作数。所以知道了这些对运算符重载函数的基本使用之后,我们就看一下运算符重载函数是如何在我们的类中使用的。
如图所示:
运算符函数的复用
搞定了运算符重载函数在类中的使用,我们现在来学一下运算符函数的复用是如何实现的,如下代码所示:
赋值重载函数
首先我们的赋值重载函数是一个默认成员函数,它的特性满足我们上述所学的有关默认成员函数的所有特性,这里就不多加赘述了,如图所示就是我们的复制运算符重载函数的实现:
重点:我们这边再来了解一下赋值重载和拷贝构造,Date d1 =d2; 和 d1 = d2;之间的区别,首先Date d1 =d2这个是拷贝构造,这个是d1 = d2;赋值重载,为什么呢?原因就是:赋值重载是对两个已经实例化完成了的对象,而我的拷贝构造则是用一个实例化好的对象对另一个为实例化的对象进行初始化。