引言:
北京时间:2023/2/5/16:38,睡了一个午觉睡到刚刚,我发现我真的能睡,也许是因为今天是下雨天的原因吧!不过下雨天确实好睡觉,也可能是因为今天起太早了,或者说昨天睡太迟了,好像只睡了7个小时。因为昨天晚上我玩了一个游戏,叫鹅鸭杀,这个游戏不怎么适合我,但是确实很有意思,游戏机制没什么意思,有意思的是跟自己的小伙伴玩,非常有意思。特别是高中同学或者初中同学,这个游戏就像是活过来了一样,变得非常搞笑。今天晚上如果该博客不能产出,原因就是有人叫我玩鹅鸭杀去了,哈哈哈!上篇博客我们把类和对象的表层给学习了一下,今天该博客,我们就延这上篇博客的内容再逐渐深入学习一下类和对象,进入类的内部。
深入C++类的内部
当我们学习了之前的有关类的基本知识之后,此时我们明白了什么类,什么是对象,所以我们现在完全是有能力自己使用C++中的类的形式,自己实现出一个封装了的栈或者队列出来的,因为类的使用上是并没有什么门槛的,不过就是把我们的函数也给写进到类中,难只是难在一些细节方面的处理上,很容易出现问题而已,并且因为我们使用类后把我们的数据结构给封装在了类中,这样我们在使用该数据结构的时候就会变得更加的安全和方便,虽然底层的实现原理都是一样的,但是我的C++类的概念就是弥补C语言中太过自由的缺陷。并且在C语言中,我们实现了数据结构类型之后,在调用过程中,是很容易把该数据结构的初始化和资源销毁给忘记了的,所以我们的C++为了解决该问题,就提出了一个默认成员函数的概念,就是你可以不需要去调用初始化和资源销毁函数,编译器会自动调用默认成员函数实现。所以接下来我们就来学习一下,C++中的默认成员函数吧!
如图我们先看一下C语言中没有默认成员函数和C++中有默认成员函数的代码区别:
类中的默认成员函数
它们的特性:如下表
构造函数主要完成初始化工作 |
析构函数主要完成清理工作 |
拷贝构造主要是使用同类对象初始化创建对象 |
赋值重载主要是把一个对象赋值给另一个对象 |
取地址重载主要是普通对象和const对象取地址 |
总:这6个默认成员函数,我们如果不写,编译器也会自动生成一个,但是如果我们写了,编译器就不会生成,而是调用我们自己实现的,注:取地址重载函数我们一般不会自己实现。
1.构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是对对象进行初始化。并且此时我们要明白一个概念,无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
总:只要是不需要传参就可以调用的构造函数,就是默认构造函数。建议每一个类我们都自己都写一个默认构造函数。
构造函数特性:
函数名与类名相同(类名就是class后的) |
无返回值 |
对象实例化时编译器自动调用对应的构造函数 |
构造函数可以重载,一个类可以有多个构造函数 |
如下图在一个类中将构造函数的特征显示的淋漓尽致:
2.析构函数
通过前面构造函数的学习,我们知道一个对象是怎么来的,那我们构建出来的对象又是怎么没的呢?此时面对这个问题,最好的解释就是用我的析构函数来解决,析构函数和构造函数的功能相反,析构函数不是完成对对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。所以总的来说就是我的局部对象在被编译器销毁之前,它会自动的调用我的析构函数来对对象中的资源(堆上开辟的空间)进行清理,只有当对象中的资源被清理完成之后,我的局部对象才可以被编译器销毁,否则就要去调用析构函数。
析构函数特性:
析构函数名是在类名前加上一个~(表示取反的意思) |
无参数无返回值类型 |
一个类只能有一个析构函数,若未显示定义,系统会自动生成默认的析构函数。注意:析构函数不允许重载 |
对象生命周期结束时(栈帧被销毁时),C++编译系统会自动调用析构函数 |
如下图在一个类中将析构函数的特征显示的淋漓尽致:
此时我们的打印框中打印了我们需要的信息,所以证明在对象被销毁(栈帧被销毁之前),我们的编译器确实是有自动去调用我们的析构函数,此时我在堆上开辟的空间等不是栈上的资源就都可以被析构函数给清理了。
拷贝构造函数
当我们了解完什么是构造函数和什么是析构函数之后,我们来聊一聊6个默认成员函数中的拷贝构造函数。但在学习拷贝构造之前,我们通过上述的知识,我们了解到了,默认生成函数在我们没有自我实现的时候,编译器会自动生成,那么此时我们会发现一个问题,如下图代码中所示:
我们可以发现在我们没有自己实现一个构造函数的时候,我们的对象并没有因为编译器会自动调用初始化函数而被初始化,这是什么原因呢?原因此时涉及一个C++中的小缺陷,C++中把类型分为了内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int、char、double等!自定义类型就是我们使用class、struct、union等自己定义出来的类型,所以因为C++中的小缺陷,此时编译器在调用默认成员函数的时候,只会对自定义类型调用,而我们的内置类型编译器是不会去自动调用默认成员函数的。所以上图由于int _year、int _month、int _day都是属于我们的内置类型,所以我们如果没有自己实现一个初始化构造函数,此时编译器并不会对我们的对象就行初始化,所以出现的就是随机值。
通过下图,我们进一步感受一下,编译器自动调用构造函数:
此时如上图所示,我们发现当我们的对象类型不是内置类型,是自定义类型的时候,我们的编译器是会自动调用我们的默认构造函数去对其进行初始化的,然后也发现,当对Stack进行初始化时,我们的Stack是一个内置类型的类对象,所以此时由于我们自己实现了初始化函数,所以我们的Stack也被初始化了。这样就很成功的实现了自定义类型和内置类型的同时初始化,也很好的证明了上述的知识。
总:在C++中我们的默认成员函数对内置类型不作处理,对自定义类型才会处理。
正式学习拷贝构造
我们在搞明白了上述的这个问题之后,我们就可以正式的来学习我们的拷贝构造了,首先我们明白一个点,就是,构造函数本质只是用来初始化的而已,而我的拷贝构造其实在本质上也只是用来初始化的而已,只是此时拷贝构造初始化的内容是从别的已经被初始化的对象中复制到该对象的数据。所以构造函数是我们自己给数据,或者编译器给数据,而拷贝构造是通过复制已经初始化过的对象中的数据。 明白了这个点之后,我们来正式了解一下拷贝构造,拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类型对象创建新对象时由编译器自动调用。
拷贝构造特征:
1.拷贝构造函数是构造函数的一个重载形式
2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会发生无穷递归(这个无穷递归是针对于编译器内部进行的,因为在编译器的内部可以理解成有无数个拷贝构造函数,所以会无穷递归)
代码如图所示:初步了解拷贝构造
深入理解拷贝构造
此时当发现不好理解拷贝构造的时候,此时我们先换一个角度,我们先从深浅拷贝的理解来理解拷贝构造,本来按照我们的理解内置类型编译器是可以直接通过赋值的形式进行浅拷贝,自定义类型编译器才通过调用拷贝构造完成拷贝。但是由于涉及到深浅拷贝问题,如下图:
通过图示,我们可以发现,此时如果是一个自定义类型,并且这个自定义类型此时指向了另一块空间,那么此时就不可以进行浅拷贝,因为如果进行浅拷贝的话,就会导致编译器按照我们的字节大小进行拷贝,那么此时就会导致两个指针,指向了同一块空间,这样就会出问题,导致该空间在最后调用析构函数释放资源的时候,该空间被释放了两次,此时程序就会崩溃。所以当我们遇到自定义类型的时候,我们不可以盲目的去调用拷贝构造,一定要注意是否涉及深拷贝问题,如果有深拷贝的话,我们要自己进行拷贝构造函数的实现。所以归在一起,不管是内置类型还是自定义类型,编译器都是优先去调用拷贝构造,从而防止深浅拷贝问题。
总:不管是自定义类型还是内置类型,编译器都是默认直接去调用拷贝构造。
重点,当我们明白了这个道理之后,我们就可以发现,只要我们进行传值传参,编译器就会去直接调用拷贝构造,根本就没有赋值拷贝这一步骤,但是当我们进行传引用传参时,编译器不会直接去调用拷贝构造,而是进行赋值拷贝,如下图:
通过仔细的观察上图,我们发现传值会调用拷贝构造,传引用不会调用拷贝构造,所以我们在上述过程中的代码中就不可以使用Date(Date d)传值传参,因为如果使用传值传参就会导致无穷调用拷贝构造,所以我们在进行拷贝的时候,就要使用传引用传参Date(const Date& d),这样就不会去无穷调用拷贝构造,而是老老实实的进行赋值拷贝。
以下内容由于现在是 1:07,避免熬夜,我们明天再写了,See you.