1.类的定义
1.1类定义格式
首先我们引入一个新的关键字-----class,class定义一个类。
定义方法
跟我们之前定义结构体非常的像
那我们来简单的看一个类的定义
我们C语言实现的时候,结构体和函数是分离的。但是现在不需要,我可以直接写 Push
现在这些成员默认是什么?公有还是私有?。私有
如果我在这儿这样写呢?
那Push是公有还是私有?
私有
它跟结构体不一样的地方在于
1
我们结构体是不是就是在结构里面定义一些变量啊?它除了可以定义变量 (我们一般叫成员变量或类的属性(基本的特征值)),还可以定义函数(也叫类的 方法或成员函数)。
第二个和以前的区别
这个类名在这个地方就是类型
那我们这个类型可以定义个对象,那我们用这个对象去访问
它可以访问Pop,不能访问Push。因为Push默认是私有的
那如果我想访问这个Push呢?
我能不能同一个访问限定符定义多个?
可以,但我们实际当中肯定不会这么玩,看起来太乱了。
再看另一个类的定义
我怎么区分参数和成员变量呢?
这样吗?
这样写就牺牲了我程序的可读性了,我怎么知道y是什么,m是什么?
所以,C++惯例上面会给成员变量进行特殊标记:为了区分成员变量,⼀般习惯上成员 变量会加⼀个特殊标识,如成员变量前⾯或者后⾯加_ 或者 m 开头,注意C++中这个并不是 强制的,只是⼀些惯例,具体看公司的要求。
在前面加一个_
不过C++并没有规范,所以就八仙过海各显神通了。
这个时候就能非常明显的区分了
我能不能把这个成员变量放上面?或者把成员变量混在几个函数中间可不可以?
都可以,C++没有限制嗷。
但是一般情况下,稍微规范一点点,都是函数在上,成员变量在下。
C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化是struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类
C++中的struct也可以定义函数,C语言不可以。
它跟class定义类几乎没有区别,唯一的区别在这里:class的默认访问限定符是私有 的,而struct的默认访问限定符是公有的。
但是C++又兼容C语言之前的用法。
我这么定义,可不可以?-----可以。
所以C++有时候定义链表的节点是这么定义的。
这个在C语言是编不过的,C语言必须得这样。
还有一种重大区别是什么呢?C语言咱们一般要typedef,因为咱们嫌写struct太麻 烦了。
C++就不需要了
但你之前的那一套,我都兼容。
那我直接这样定义可不可以呢?--------也可以。
一般情况下,我们还是喜欢用class,只有一些少数的场景会用struct。什么场景 呢?比如你所有成员都是公开的。
定义在类里面的成员函数默认为inline
也可以进行声明和定义的分离。
把定义写类外面,但这样我这些变量找不到啊(C++不是默认只会在局部和全局找嘛)。
所以定义得这样写
你得指明,我这个函数不是一个全局函数,我是一个类的成员函数,只是声明和定义分 开了。
1.2访问限定符
C++类这个地方除了把数据和方法都放到一起,它还增加了一个叫访问限定符的东西。
访问限定符有3个
public(公有)
private(私有)
protected(保护)
它们在现阶段没有区别,在继承以后才会有区别。
C++⼀种实现封装的⽅式,⽤类将对象的属性与⽅法结合在⼀块,让对象更 加完善,通过访问权限选择性的将其接⼝(就是函数)提供给外部的⽤⼾使⽤。
我呢,首先把这些数据和方法呢,我都放到了一起。放到一起了以后,我还想做一 些所谓的限制。
如果我想给你类外直接访问的,我就定义成公有,否则,我就定义成protected和 private。
那访问限定符是怎么限定的呢?
从该访问限定符到下一个访问限定符出现为止。
如果后面没有访问限定符,那就到类结束
class定义的成员如果没有访问限定符,默认是ptivate,struct默认为public。
一般情况下,我们会把成员变量限制成protected或者private,需要给别⼈使⽤的成员函数会放为public
也就是说,我不希望你随便修改我的数据。
1.3类域
我们之前说C++在有全局域,局部域,命名空间域,还有一个就是类域。
类定义了以后也形成了一个新的域。
为什么要形成一个新的域呢?
因为不行形成新的域也有问题啊。
你想想,你定义一个栈,你是不是有一个叫Push的函数啊?那我定义一个队列,我也有一个叫Push的函数啊。
那我们两个函数会不会冲突呢?不会冲突。因为我们在不同的域里面。
类域和命名空间域只影响名字隔离,不影响生命周期。
如果你在类外面,得指定类域
这个刚刚我们讲过了(声明和定义分离)。
类域影响的是编译的查找规则,下⾯程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪⾥,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找
#include<iostream>
using namespace std;
class Stack
{public:// 成员函数void Init(int n = 4);private:// 成员变量int* array;size_t capacity;size_t top;
};
// 声明和定义分离,需要指定类域
void Stack::Init(int n)
{array = (int*)malloc(sizeof(int) * n);if (nullptr == array){perror("malloc申请空间失败");return;}capacity = n;top = 0;
}
int main()
{Stack st;st.Init();return 0;
}
类域没有展开using之类的说法嗷,命名空间域才有
2.实例化
2.1实例化概念
实例化其实我们也很好理解。
问大家一个简单的问题:这个东西是声明还是定义?
对于变量,声明和定义的区别是什么呢?-------开不开空间。
定义开空间,声明不开。
它是声明。
我能不能这样去访问它?
不能,因为它是声明,它都没空间你咋能这样访问呢?
它是声明,它没有开空间
什么时候才会开空间呢?
这儿是一个类型,用这个类型定义一个对象
在官方的说法叫做实例化嗷(就是产生一个具体的实例嘛),就是用这个类型实例 化出一个对象(翻译的问题)。
类型和对象的关系是一对多。一个类可以实例化出n多个对象。
怎么理解呢?
类就像我们的设计图一样,我们要建房子,我们有个设计图。这个设计图它是按比例缩小的,通过这个设计图,我可以看到我的卧室有多大,我的客厅有多大,通过图纸,我可以看到我房子的一切数据。
但是图纸里面能不能住人?不能。
我用这个图纸可以修出n多栋房子。
这个房子是不是就可以住人了?
所以用类实例化出对象,就和我们用图纸修房子是一样的。
2.2对象大小
那这个对象的大小怎么算的?
我们简单回顾一下,C语言之前讲结构体里面是不是就存的成员对象,这些成员的大小 要按照内存对齐的规则来计算。
那现在也是,不同的地方在于,我们要不要把成员函数的函数指针(函数本身肯定是不 用存的,在库里)存到对象里呢?
不需要,对象里面只要存成员变量即可。
为什么不需要?
我们用date实例化两个对象
d1和d2的年月日是不是一样的?不一样是不是?你d1得有你d1的年月日,d2 得有d2的年月日。
那大家再想,d1和d2调的这两个函数是相同的函数函数还是不同的函数?
我们说,不同的对象,他们的成员变量是不一样的
但我们的函数是不是一样的?是一样的
那我们的函数是一样的,我们在对象里面都存一份,这个时候会不 会有浪费啊?
其实我们透过汇编这个层面上也能看到在这个地方是不是一样的。
我们说函数被编译完了是一段指令
函数指针可以认为是第一句指令。
那这个函数指针存在哪儿呢?它会存到一个公共的地方。
这⾥需要再额外啰嗦⼀下,其实函数指针是不需要存储的,函数指针是⼀个地址, 调⽤函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地 址,不是在运⾏时找,只有动态多态是在运⾏时找,就需要存储函数地址,这个我们以 后会讲解。
上⾯我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对⻬的规则。
-
内存对⻬规则
-
第⼀个成员在与结构体偏移量为0的地址处。
-
其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
-
注意:对⻬数 = 编译器默认的⼀个对⻬数 与 该成员⼤⼩的较⼩值。
-
VS中默认的对⻬数为8
-
32位下默认是4
-
64位下默认是8
-
-
结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍。
-
如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩
-
就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍。
-
那我们来算一下
A
B,C是0吗?
如果是0,你这个b对象和c对象怎么定义出来的?
我们说定义是不是就要开空间啊?如果我对b再取个地址,那我取个地址了以 后,地址到底是多少啊?
难道是空指针?都没有大小,那它地址是不是只能是空指针啊?
那我们看到实际过程当中,它再这儿的大小不是0
我们刚才其实已经分析了,如果给0在这个地方其实挺难受的,你的地址到底 是多少?
比如你定义了b1,b2,b3,它们的区别是什么?你怎么表示它存在过?因为定义 就要开空间啊。
所以我们看到,如果类它没有成员变量,我们都可以把它们简称为空类或没 有成员变量的类。它们的对象大小是1。
开1字节不是为了存储什么数据,因为如果一个字节都不给,怎么表示对 象存在过呢?
所以这里给一个字节呢,纯粹是为了占位,表示对象存在。
这个时候你要取它的地址啊什么都好取。
那我们以后会定义这样的类吗?会定义,而且还不少。
以后我们会学习一个仿函数的类,这个类几乎全是这种没有成员变量的 类。
为什么要内存对齐啊?
内存对齐不就浪费了吗?方便查找(空间换时间,提升效率)。
怎么方便查找?
如果不对齐会有什么问题?什么情况下会有问题啊?
首先,大家要理解内存对齐,要先知道一个东西。我们CPU来读取内存 数据的时候,他不是能够从任意位置开始读的。它在这个地方读的时候,它 一次有一个CPU的字长(不同的机器不同)。
那它一次性读的时候,并不是说每次都能从任意位置开始读。
如果我能从任意位置开始读,读任意大小,那左边的设计毫无优 势,还浪费空间。
实际当中,在读取数据的时候,它是从整数倍位置开始读,一次读 4个字节(这个和具体的机器有关系,我们在这假设读4个)。
为什么这样设计呢?
读取这些数据它是用一个叫数据总线的东西。
比如说4个字节(32个比特位)就有32根数据总线,因为在内存当 中就是0101这样的类似电信号位这样的方式取存储的嘛,那我在这个地 方是不是就是看它到底是0还是1啊?
你就可以认为我就像一辆车,我一次就需要拉32个人,你如果从任 意位置读任意字节的话,我去这儿读一个字节是不划算的(意味着我一 次只能拉4个人),所以我就规定我在这儿拉人,我一次就拉32个人。
我拉人的那个起点是规定的,就是从这个点开始拉,然后一次呢, 不管你要拉多少人,我都给你拉32个人过来。
也就意味着,它的读取都是这个位置开始读,都四个字节(假设 嗷,实际不是这样的,实际和具体的机器都有关系)。
那也就意味着从这个位置开始读4个字节,不管是左边还是右边, 都是这样读的。
那大家看在这个地方,我们在这个地方访问这个数据的时候。
如果是左边这样的设计,我读了4个字节,我只要第一个字节
如果是右边这样的设计,我要读这个_i,我能不能从第一个字 节这个位置开始读4个字节?
不能,因为它必须从整数倍的位置开始读。
那就只能从这个位置开始读,读4个字节,留下后3个字节。
再从这个位置开始读,读4个字节,留下第一个字节。
再用第一次读到的3个字节和那一个字节进行拼接,拼出的这 个字节才是我们的_i。
我们得读两次才能读取到我们的_i,而且很麻烦很复杂。
所以,基于这样的原因,内存对齐是不是就有它的优势啦?
3.this指针
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调⽤Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这⾥就要看到C++给了⼀个隐含的this指针解决这⾥的问题。
那这个隐含的this指针是什么情况呢?
其实你看到的这些函数,它的第一个参数不是year,Print也不是没有参数
它们都有一个隐含的参数,这个隐含的参数,规定叫this
this也是一个c++新增的关键字。
那这个隐含的this指针是谁呢?在调用的时候,这个位置它会悄悄的把d1的地址传过去。
那这个时候我就知道谁是d1,谁是d2了呀,因为this就是指向你这个地方的调用对象的。
那这个时候Init和Print的是不是就是d1的年月日啊?
怎么做到d1的年月日的呢?
还有一步。这里面访问年月日其实是通过this访问的。
这个时候就能完美的做到区分了。
this 指针是一个隐含于每一个非静态成员函数中的特殊指针
C++规定不能在实参和形参的位置显式的写this指针(编译时编译器会处理),但是可以在函数体内显式使用this指针。
也就意味着,如果你想在这儿显式的写,不行
实参
形参
但是,我在函数体内可以使用这个this指针
为什么会这么规定?
实参形参让你自己写会变混乱,不如让编译器自己去搞。
在函数体内有些地方必须要用this,我们后面会看到,你不写,他会自动给你加上,你写了,它也可以兼容。
this指针是不能修改的
因为this指针是const修饰的
但它指向的内容是可以修改的
this指针是存在哪个区域的?
是不是存在对象里面的?
一定不是存在对象里面。
我们刚刚讲了,算对象大小的时候你有没有算this指针?----没有吧。那this指针凭什么 在对象里面?
那在哪个区域比较合理呢?
它是一个形参,那形参这些参数是存在哪儿的呢?
栈帧里面的对不对?
在VS下,由于this指针频繁使用,它存在了寄存器下。所以有些地方说它放到寄存器也是对的啊。所以这个地方要具体问题具体分析。
传统意义上我们还是认为它是形参,存在栈帧里面。
this指针的生命周期是和对象本身紧密相关的
我们来看俩题
那到底是运行崩溃还是正常运行呢?
这个题是正常运行
为什么是正常运行,没有产生崩溃呢?
这个空指针这样走难道没有解引用吗?
这个时候就不得不抛出一个问题了
编译器被编译了以后,底层都会干嘛?是不是都会转换成汇编指令啊?
按我们刚刚讲的,这个地方转换成的汇编是什么?
是不是call一个地址啊?
那这个地址在哪儿?在不在对象里面?
不在,因为我们前面讲了,成员函数的指针不存到对象里面。
成员函数编译,编译好了是一段指令,它在符号表里面,编译的时候,通过函数名 就确定了这个地方的地址,和这个对象没关系
那为什么需要对象去调用呢?
一个是对象类域嘛,它是属于它的成员函数。
那你在这儿p调用,我是A类型,编译的时候它就知道去A类里面去找。
其次,要传递this指针。
调用函数是不是要传参数啊?(传参数,以前是lea,因为是对象,lea是取地址 嘛)。
这个地方需不需要取地址?不需要取地址了,因为p就是对象的地址啊(这儿 是对象的指针啊)。
这个地方应该是这个,ecx相当于存的是this指针
然后再call这个地址
过来就调用这个函数,这个函数里面的this是空指针
空指针会报错吗?
我有一个空指针,我没有去解引用访问,会报错吗?不会。
不要看到箭头就以为要解引用。
这个时候会不会解引用啊?(解引用是指获取指针所指向地址中存储的值的操作) 会。
因为成员变量是存在对象里的。这个时候才会崩溃。
这个会崩溃
4.C++和C语⾔实现Stack对⽐
面向对象有三大特性:封装、继承、多态
我们来对比一下C和C++
这里是我们C语言实现的栈
C语言
数据是放到结构体
函数是函数
函数要访问数据,得把数据的地址传过来
这是我们当前学的知识(仅限于当前学的知识哦)定义的栈
C++:数据和方法都放到了类里面
形态上是不是发生了一些变化啊?但是本质变没变?
没有变化
在我们这个C++⼊⻔阶段实现的Stack看起来变了很多,但是实质上变化不⼤。等着我们后⾯看STL中的⽤适配器实现的Stack,⼤家再感受C++的魅⼒
C++类的设计最大的变化是这个面向对象三大特性之一-----封装。
面向对象在这里提出了三大特性
封装
这里呢,真正的体现是这个封装。
C语言它是比较自由的。它的数据是随便访问的,方法也是这样的。
举例:我们在这儿访问栈顶数据,我可以调STTop()。
但它也能这样
所以就导致了用C语言实现这些东西的时候很不规范。
C++就不能采取这样的方式去访问数据了
为什么?
你访问不了
因为我们C++建议把成员变量都搞成私有的,这就在语法上限制了你在 外面不能随便访问我的成员变量。
也就是说,你想访问我的成员变量,你得通过成员函数,哪怕像 TopSize()这样的(就一行代码)你也得通过成员函数来访问。
那我的成员函数只要实现的没错,你就不会乱搞。它是更规范的。
C语言数据和方法是分离的,也没有任何访问限定符。C++把数据和方法放 到一起。想给你直接访问的,放成共有。不想给你访问的,放成私有。
这是一种封装规范管理。C++中数据和函数都放到了类⾥⾯,通过访问限 定符进⾏了限制,不能再随意通过对象直接修改数据,这是C++封装的⼀种体 现,这个是最重要的变化。这⾥的封装的本质是⼀种更严格规范的管理,避免出 现乱访问修改的问题。当然封装不仅仅是这样的,我们后⾯还需要不断的去学 习。
继承
多态
我们后面会讲。