引言:
北京时间:2023/2/4/9:52,起床时间8:55,今天为什么可以起这么早呢?原因就是我没有把我的闹钟给放在枕边,哈哈哈!但是好像导致一个8:20的闹钟闹了半个小时,哈哈哈!上边博客带我们大致了解了一下什么是类和对象,然后由于时间安排问题,没有写完,今天,我们就接着上篇博客,来对类和对象,以及相关知识做一个完整一点的学习。
C++中的类和对象
该说不说,在学习C++中的类和对象的时候,我们第一是要搞清楚什么是类,什么是对象,然而,这两个东西我们其实是了解一点的,只是跟我们以前了解的,有了一些的改变,所以在我们学习类和对象的前提下,我们先去复习一下有关C语言中的结构体的知识,使我们可以更好的掌握C语言的语法和学习接下来的类和对象。
复习C语言中的结构体
下图就是C语言中如何定义一个结构体的代码,从最简单的结构体到C语言中最完整的结构体定义,当然在结构体中我们还可以增加各种的变量,这取决于你需要的数据结构的类型
并且当我们复习完了如何定义结构体之后,我们复习一下如何使用结构体,如下就是对普通结构体的一个最基本的初始化,和打印结构体中的内容的代码,我们通过看代码的方式来回忆我们以前对结构体的认知,从而达到复习结构体的效果。
如图:
正式开始C++中的类和对象
当我们复习了C++中有关结构体相关的知识之后,我们现在就可以来看一下C++中的类是什么了,其实类的本质上还是一个结构体,只是此时在C++中叫做类,在C语言中叫做结构体,当时结构体不等于类,类是结构体的进化,为什么说是进化呢?因为我们都知道,在C语言中,我们的结构体中只可以定义我们需要的结构体成员变量(如:int、char、int*)等,而在C++中,我们的类中,此时不仅可以定义结构体成员变量,而且此时还可以定义函数,就是将函数的定义,直接写进我的类(结构体)中,所以C语言中结构体和C++中的类最明显的区别就是类中可以定义结构体。
如下图所示:
当然除了上图中类中可以定义函数这个特点之外,我们C++中的类和C语言中的结构体还是有很大的区别的,例如:我们以前在实现链表的时候,我们需要一个一个相同类型的结构体构建,此时只能在链表结构体中加一个该结构体的指针,此时的指针是struct ListNode*next
类型的(不可以使用typedef之后的,必须要加上struct),但是此时在我们的类中,我们定义了一个类之后,此时struct后跟的就不再是只是类的名字了,此时跟的就是类,所以就可以把struct后面的名字当作类来使用,不需要再像C语言中的结构体一样加上struct了。结合下图,就很好理解了。
如图:
了解了上述知识,我们大致知道了什么是C++中的类了,接下来就让我们更近一步,看一下类在不同的文件中是如何定义使用的,如下图,我们看一下类在test.cpp和test.h中的类的成员函数声明和定义分离是怎样的。关键就是注意:搞清楚归属,该函数是全局函数,还是类中的函数。关键点:(::)
类的访问限定符及封装
类的访问限定符是什么,相信大家都有这个疑问,接下来让我们带着这个疑问,我们来看一看什么是访问限定符吧!如下图代码,我们会发现,当我们使用了class(类)来构建我的代码时,是没有问题的,但是当我想要调用这个类中的函数的时候,编译器却报错了,这是为什么呢?此时答案就涉及到了我们的访问限定符的概念了。
C++中的访问限定符
|public修饰的成员在类外可以直接被访问 |- |**protected和private修饰的成员在类外不能直接被访问** |**访问权限所用域从该访问限定符出现的位置开始直到下一个访问限定符的出现为止** |**如果后面没有访问限定符,作用域到}的位置结束** |**class的默认访问权限为private,struct为public(因为struct要兼容C语言)**所以通过上述的知识,我们知道了class的默认访问权限为private,所以我们可以知道,我们上述代码出问题的原因就是我们使用了class,但是我们没有使用public权限,我们不能从class中拿到我们的函数,因为class的默认访问权限为private(私有的)。
改进代码如下:
此时加了public就和我们想象中的一样了,我们可以在class的外部使用class内部的函数了。并且一般我们的类属性是类私有的,类作用有的才是公有的,所以类中的东西,私有公有都是由我们自己决定的哦!
严谨编写代码
我们此时了解了上述知识,会发现一个问题,我的参数和我的类属性(成员变量),有时会出现一定的问题,就是类属性容易和我的参数的名字相同,导致我们对参数和类属性有歧义,所以此时我们在编写代码的时候,通常我们会给类属性的前面加上一个(_)来表示该变量是一个成员变量,而不是参数。如下代码:
类的封装
当我们知道并且会使用我们的访问限定符的使用,我们接下来来理解理解我们的类的封装,此时我们了解了封装,我们就顺便谈谈面向对象的三大特性:封装、继承、多态,所以我们就带着这三大特性去学习接下来的C++中的类和对象吧!首先我们这里就来讲讲我们的封装,什么是封装呢?**封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口和对象进行交互。**并且封装的本质就是一种对类的管理,使用户可以更好的使用类。
类的作用域
其实上述我们有讲到一些什么是类的作用域,此时我们在展开来讲一下,当我们定义一个类(结构体)时,我们定义的类就是一个新的作用域,类的所有成员都在类的作用域中,在类的外部定义类成员时,此时就不能直接给你使用,因为类是有自己的作用域的,所以此时就要使用一个操作符(::),这样就可以指明该成员属于的是那个类的作用域。
代码如下:
类的实例化
搞定了上述的知识,此时我们就来了解一下什么叫做类的实例化,但是在我们理解什么是实例化之前,我们先看一下下面代码,我们再来深入了解一下类中的成员变量。
如下图:
此时可以看出,我们上述代码是一个类,在这个类中有三个成员变量,很多同学会认为这个是属于变量的声明,因为平时我们在函数中,无论是普通函数,还是main函数,我们在该函数内部定义一个变量都是真正的定义,因为在这些函数中,只要一定义函数,就有相应的栈帧空间来存储该函数定义的变量,所以在函数中,我们使用各种类型定义的变量都是属于变量的定义,不是变量的声明,然而此时我们并不是在函数之中,而是在class(类)中,因为是在类中,所以此时并没有想函数一样的栈帧空间或者动态空间甚至静态空间来存放我们声明的变量,所以此时的变量就不属于变量的定义,而是属于变量的声明,所以如图中所示,我们的成员变量只是声明,并不是定义。
总:在类中的变量,由于没有空间存储的原因,所以只是声明,并不是定义。
此时我们明白了上述的这个小知识,我们就可以来正式了解一下什么类的实例化了,类的实例化本质就是使用我们的类去创建一个对象(变量),此时使得这个类得到栈帧空间,此时类就被存储到了我们的内存之中,此时就实现了我们的类的实例化,所以这也就是我们类实例化的概念。
如图:就是一个类的实例化的代码
这样,我们就可以通过类的实例化使类获得空间,从而让我们可以使用类。明白了这个道理之后,有的同学可能就很好奇,类中不是还有函数吗,那么此时的函数到底是声明还是定义呢?其实这个问题是非常的简单的,从上述的内容,我们就可以知道,我们的函数是可以直接从栈上获取栈帧空间的,所以类中的函数是属于定义而不是声明,形象点,我们通过计算该类的所占内存大小来看到答案,可以猜测如果类所占的内存大小为12,那么该类中确实是只包含了成员变量空间,如果不是那么该类中就有包含函数空间,那么函数就是属于声明而不是定义,所以如下图:
可以看出此时和我们的猜想是一样的,所以得出结论:类中的函数因为没有占用对象的空间,而是自己形成栈帧,是定义,而不是声明。 并且此时我们可以联想到一个问题,就是为什么我的成员变量是存在对象空间中,而我的成员函数不是存在我的对象空间中呢?原因:可以节省我们对象所占内存的大小,并且把定义的函数放在一个叫代码段的地方,使其实现公共(代码段就是一个公共区域),这样,我们每次使用同一个函数的时候,就不要重新在对象空间中开辟空间了,而是可以直接从代码段中获取同一份空间的函数,这样就使我们的内存空间得到很好的使用,并且提高程序效率。
总结:每个对象(d1(变量))调用成员变量是不一样的,调用一次成员变量(公开情况下),就需要在对象上开辟一块空间(大小由类型决定),实现变量数据得到独立存储。但是每个对象调用成员函数是一样的,因为我们的成员函数是放在公共区域(代码段)。
计算类所占内存大小
知道上述知识,我们来尝试计算一下类空间应该要所占的内存大小,如下图也:
此时根据上述知识,成员函数是放在代码段中,只有成员变量是放在类对象中,那么此时答案是多少呢?4 、0、 0 吗?正确答案是4、1、1 ,原因是:只要我们的类被实例化之后,就要开空间,至于空间开多大是由类中的成员变量决定,所以就算我们的类是个空类,此时只要其被实例化,那么该类就会被存储在栈帧上,该类的地址才会被存储,所以空类所占的空间是1 ,可以理解成这个1,就是用来标示我们的类,证明该类存在过。
我们顺便复习一下C语言中的内存对齐的概念:
第一个成员在与结构体偏移量为0的地址处 |
---|
其它成员变量要对齐到某个数字(对齐数)的整数倍的地址处,VS中对齐数默认是8 |
结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐数参数取最小)的整数倍 |
如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍出,结构体的整体大小就是所有最大对齐数(含嵌套结构的对齐数)的整数倍 |
什么是this指针
想要学习什么是this指针,此时我们就要先了解一个概念,这一系列的概念都是和我们C++中类这个东西有关的,一切都是从类的使用延伸出来的问题,例:在类中,我们通过上述知识得知,类中的函数在类被实例化之后,函数并不存储在类对象中,而是存储在特定的代码段中,存储在代码段中,起一个共享的作用,那么此时经过这个现象,我们可以发现一个问题,就是当我使用该类创建了两个类对象出来时,并且此时用这两个类对象同时调用了类中的某一个函数,那么此时该函数是属于那一个类对象呢?该函数存储在那一个类对象的空间中呢?所以我们从这个问题出发,我们就可以很好的去认识一下什么是this指针了。
如下图:
此时出现了问题,我们就要来解决这个问题,如何解决呢?其实我们的编译器已经是帮我们解决了编译器在识别出我们调用了代码段中的共享函数的时候,编译器自己会生成一个this指针,通过这个this指针来标示我们的调用对象,从而确定该函数是被那个调用对象给调用。并且本质上我们的this指针就是成员函数的一个形参。所以this指针是存在该函数的栈帧上的(不可以认为是存在对象中的,因为内存大小并没有算this指针)
如下图:
所以这样我们的编译器就可以很好的通过this指针来区分谁是谁调用的函数了。
并且此时我们通过上面的这幅图,我们可以看出,,我们打印了我们的this指针,发现我们的this指针确实是一个地址,强调,我们的this指针确实是一个地址,并且发现,我们调用了两次Init
函数,确实是执行了两次不一样的函数调用,然后生成两个不一样的地址,并且这两个不一样的地址,就是我们的d1和d2的地址,也就是我们调用对象的地址,所以现在可以充分证明,我们的this指针可以帮我们区分出共享函数到底是属于那个对象。
此时我们通过理解图中的两句代码,来深入理解一下this指针:
此时想解决第一条代码,这个一定要回想起我们this指针的知识,因为this指针的作用就是让我们知道该函数是被那个对象所调用(这里this的作用就是让我们知道,我们的函数是被ptr这个对象所调用),但是又由于ptr->Function();
ptr指针已经指向了我的Function了,我已经知道我的Function是被ptr这个指针所调用,那么此时this指针就已经相当于是ptr指针,两个指针起着一样的作用,所以该句代码是没有问题的,目的只是为了搞定楚Function是被ptr指针调用。但是我们看第二句代码可以发现,它和第一句代码是一样的,同时使用了ptr指针作为this指针来表明此时它是使用ptr指针来调用我们类中的共享函数Init
,这样做本来是合理的,因为ptr指针就是this指针,但是我们又发现在调用Init
函数的时候,我们把参数也传递给了我们的调用函数,此时就会导致这些参数会去使用我们的this指针去访问我们的成员变量,然后把参数数据给赋值给我们的成员变量,但由于此时该this指针已经变成了ptr指针,变成了一个空指针,我们拿着这个空指针去访问我们的成员变量,等于是用空指针去对成员变量进行解引用,此时自然而然是不可以的,所以第二句代码本质是一个错误的代码,原因就是:使用了空指针去访问成员变量。