目录
对面向过程和面向对象的初步认识
类的引入
封装 和 类的访问限定符
所以祖师爷在类中还引入了访问权限
用类定义变量
类的理解和对象的实例化
sizeof 计算类对象的大小
类对象的成员函数在公共代码区
this 指针
-
对面向过程和面向对象的初步认识
什么,是面向过程?
那生活中的洗衣服来做栗子:比如手搓洗衣服这件事,这个过程中,包含了
拿衣服 → 拿洗衣粉 → 放水浸泡 → 用洗衣粉手搓 → 涤第二遍 → 晒衣服
面向过程在这里是更关注洗衣服每个步骤的过程,关注的是过程
但是这样的话就要我们的使用者就得比较有要求,你得会洗衣服,要有耐心等
而对应到我们编程的面向过程,关注的也是过程,这样可能很多方面关于对象(使用者等)的考虑可能就会欠佳,并且对于对象(使用者等)可能就有更高的要求
而面向对象呢?
面向对象同样也用洗衣服为引例:这里更像是用洗衣机洗衣服
我们拿要衣服 → 放进便利我们、面向我们的洗衣机 → 晒衣服完事
我们的面向对象在这里更关注的是我们,让我们更方便、系统地洗衣
这样的话对于对象(我们使用者)来说,是很有方便,系统有效的
而对应到我们编程的面向对象,关注的是对象。更面向于使用者,这样可能会让过程更麻烦与否,但是这是面向于对象,服务于对象才是我们的目标 在人的世界中可以说面向对象是优于面向过程的
-
类的引入
在C语言中,相比其他语言最大的特点之一,是太自由了,我们的数据(变量等)和 方法(函数等)是分离的。比如用C语言实现的一个栈
栈栈
可以看到,不管是 StackPush 、StackPop 还是其他函数,函数是和结构体分离的,而数据一般捆绑在我们的结构体中,其结果是函数和我们的数据是分离的。这样的函数与数据分离的结果是什么?我们可以拿到任意局部的数据去进行修改,或者不修改;只要方法正确,我们可以没有限制操控地、细致地操控很多东西……这就是自由,没有过多的限制同样决定了C语言是更关注过程的一门语言,我们可以用它实现很多非常巧妙的函数(那些很优很优的算法很多都是C语言实现的)
但是世界是发展的,更多事物是服务于人,计算机也是;所以只有面向过程是不够的,我们也需要一门更加面向对象的语言,去服务于我们,或其他事物,也能让我们更加方便与规范
C是满足不了,但是祖师爷可以
—— 所以 类 ,诞生了
类的格式:
class Stack { //public: Init(); //函数这里没展开实现 Push(); Pop(); Destroy(); //private: int *_p; int _top; int _capacity; }; |
理解李姐:
在C语言中,因为结构体中只能是捆绑数据,所以定义了一个结构体变量更相当于创建了一个集成的数据,形成一个大的数据域,它更面向于我们的函数,可以说是更面向于过程;
但是现在,我们的类是将数据和方法都捆绑到一起,函数也可以和数据捆绑起来,这样我们用类定义的一个变量更相当于一个对象,类中有的函数可以通过这个对象去调用,相当于对象在行使它所拥有的权力。假设我们用类来实现商家这一类群体,类中的数据可以是商家的个人数据,可以用我们熟悉的数据结构来进行存储商家信息作为我们的数据;类中的函数就是商家可以行使的权力,比如修改自己的信息(修改自己对应的数据部分),货品上架等,这些是类中的函数完成
—— 哦哦
定义变量且不看,先来了解面向对象的第一个特点:封装
封装 和 类的访问限定符
要了解封装,害(hai弟叁牲)得先从自由人C语言讲起
C语言的结构体中数据和方法是分离的,肥肠自由
—— 但是,自由性很高也往往不是一件好事
面向过程的自由人可以完成很多细致的操作,很多高级的函数;函数很优固然是好,但是对象是我们,我们是使用者,越细致的操控意味着使用者要注意的地方有很多,对于对象的要求也就越高:
栗子:
可能你有 必须严谨包装、在36~46度生存的化学限量版洗衣粉,效果虽然很好但是使用者一旦操作不当就会出错;
或者说刚刚的栈,栈本身是后来入栈的元素在栈顶上,但是自由度高有的对象就有可乘之机,挪栈中全部元素空出栈底,把后来的元素放到栈底,本来你应该在栈顶的呀;如果说这样在一些情况下可以有利,那没事;但是如果是一个不能插队的排队系统呢,有人这样干呢!!后面的元素会想刀人的,关键在代码实现层面没有有效的办法去管控它(可以但是可能会异常繁琐)
再举一个刚刚的例子(栗子),刚刚实现的栈中,栈的数据部分捆绑在一个结构体中,结构体中的top有两种初始化方式:
top = 0; 表示 top 指向入栈位置
top = -1; 每次入栈时, top要先自增,再进行入栈元素的赋值:相当于top指向的是入栈位置的前一个位置
现在在看到我们的入栈函数,其核心代码也就只有一行两行;但是有的人看到了说直接在要使用的时候直接 arr[++top] = 入栈元素; 但是一运行发现出错了,最后调试发现 top 初始化为 0,就指向入栈位置,应该这样写:arr[top++] = 入栈元素; 这也是一种不规范的行为
这里我们可以看到:如果数据和方法分离(比如刚刚的不调用函数直接入栈),我们自己要控制的也越多,有些人也不会讲规范,很容易出错。对于个人是很讲究代码规范的,看到刚刚的直接入栈那种不规范操作更是嫉恶如仇
我们看到,C语言这样数据和分离的方法在面向过程中是非常牛的,很多很多问题最优解也是用C语言写的;
但是数据和分离的这种方法在对于对象来说,很多时候对于对象的要求就要求会很高,有些人会很不规范,对于不规范操作我是嫉恶如仇,当然祖师爷也是
祖师爷想,既然C语言那么多人会代码不规范,提高每个人的素质比较困难,但是可以改变语法
—— 《既然改变不了世界,那就改变自己篇》
所以祖师爷在类中还引入了访问权限
开始了开始了
祖师爷设计了三种访问权限:
public:公有的。类中成员的属性为 public 表示该成员可以在类的外部直接被访问
protected:被保护的。类中成员的属性为 protected 不能在类的外部直接访问该成员
private:私有的。类中成员的属性为 private ,表示该成员不能在类的外部直接被访问
用法简单,如下:
属性为 public 的成员可以在类外面直接访问,被 private 修饰的成员不能被直接访问(这里错误展示就不写啦,有点小锉锉,不是理由)
一般我们把要给对象用的成员函数属性设置为 public ,而不想让对象改动的数据设置为私有 private ,这样对象在类外就只能调用你规定好的函数,让使用者更加规范地、方便地、系统地去访问数据等操作,将不规范扼杀在了摇篮里
这样设计带来的规范和方便时给人的,所以这里也体现了 面向对象(关注的是对象)
其实了解了这个,大概想想就可以猜到封装的意思了
类的封装:将数据和方法有机结合,不提供对象的属性和细节,只提供对象成员函数的接口,以此来进行和对象的交互
(不提供 对象 成员函数的实现细节 和 各数据 ,只给你调用成员函数来对对象的数据等进行修改)
这样很大程度上提高了我们的代码的规范性,用起来好方便,呜呜祖师爷真好,祖师爷以德报怨,爱死祖师爷了
现在类的基本格式和定义搞清楚了,来看看怎么用类来定义变量
用类定义变量
和C语言的结构体不同,结构体在定义变量时要加上 struct,不能直接是结构体名字来定义变量
而在类中,是直接使用:类名 + 变量名 来定义变量 (etc. Stack S; )
(大概是祖师爷也觉得每次定义要加struct太麻烦了,直接改掉了)
etc. //与上面格式代码对应
Stack S;
对的,just这样方便
但是有人发现在C++中使用 struct 定义的类型也可以直接用 类型 + 变量名 来定义变量
那是因为在C++中,struct 定义的类型也被升级成了类;也就是说,C++使用 struct 和 class 定义的都是类,定义的不是结构体哦
那有人说,struct 定义出来的类和 class 定义出来的类没有任何差异吗?有,但不多
就一个地方:
struct 定义出来的类,其成员的属性默认为:public
class 定义出来的类,其成员的属性默认为:private
就一个公私有的问题啦,不过日常我们的数据不愿意被外部直接访问到,如果忘记手动修饰成员的属性,class 会更安全健壮点
类的理解和对象的实例化
思考,只是定义好了一个类,它里面的数据有没有开空间?
早在C语言的结构体中,刚定义好的结构体类型只是声明,其中的成员变量没有分配空间,只是声明,声明这个结构体类型里有这个成员;而到了真正定义了一个结构体变量时,才根据刚刚的声明去开辟空间,按成员声明分配空间
C++中刚定义好的类也是声明,其中的变量是声明该类中有该成员,并没有真正开辟空间
注意,这里的成员变量 _pa _top _capacity 还没有开辟空间,只是声明,声明此类中有该成员变量
那在什么时候初始化呢?
在对象被定义的时候,也就是在对象实例化的时候,才真正在本函数栈帧中(或其他存储空间中)开辟大小为 类的大小 的空间,按成员变量的声明依次分配内存空间给它的成员变量
形象地加深理解:
我们可以看到,其实头文件中的类更相当于一张图纸,一张蓝图;而用这个类创建了对象,才是用这个图纸将物体创造出来,因为创建好对象才真正开辟了空间,一定要理解清晰,每次写之前就应该想到
用类来创建对象的时候才分配空间,所以给这个过程一个形象的名字,叫做对象的实例化
继续添加内容
sizeof对象的问题,图纸和公共代码区
sizeof 计算类对象的大小
要讲到计算一个类对象的大小,得先了解:
类对象的成员函数在公共代码区
和结构体计算大小不同,类中除了有数据,比结构体还多了成员函数这一东西
那计算大小时要不要算进成员函数的大小去呢?
那得看对象的栈帧里面是不是有成员函数的栈帧
先构想:如果成员函数的栈帧在对象里面,那创建的每一个对象内部都有该函数,用的时候调用它。这样固然可以,但是重复的那段函数代码是不是很冗余啊?这样设计是不是很挫啊 —— 祖师爷当然不会那样干
这就好比一个对象相当于一个房子,然后成员函数相当于日常大家都要用的足球场,现在如果在对象里面都塞一个函数的空间,相当于每个房子里都装修一个足球场
祖师爷怎么做呢?祖师爷把成员函数的栈帧空间放到了公共代码区,不在单个对象里面,而是每个对象(同一类)要使用该函数时,都去公共代码区调用该函数,相当于只要定义一个函数栈帧 大家一起用。这就相当于把足球场 修在了小区里面,每个房子的人可以使用同一个
所以成员函数的函数栈帧是不在对象里面的,而是在公共的代码区,所以计算大小时,也不用计算每个对象里面成员函数的大小
所以剩下的就只是数据了,这下和C语言结构体计算大小就一样了
-
this 指针
类中的私密数据不是不可以在外部访问到嘛,调用成员函数才能访问私密数据,那编译器是怎么实现的呢?它的底层又是什么呢?
先来看函数对象调用成员函数的一些特点
我们看到当类的对象调用其成员函数时,格式是:对象.成员函数(传参);
调用的汇编代码如下:
其实对象调用成员函数的底层还会有一个 this 指针,这是属于每一个成员函数的浪漫(每一个成员函数都有这个 this 指针)
this 指针 是一个隐含的参数,比如在调用 S1 中的 Push() 函数时,this指针 是一个隐含的参数
在调用 S1.Push() 时,编译器会 取 S1的地址 给 this指针,等到了成员函数内部,就可以用这个 this指针 可以用来访问 S1 中的数据(权限够),这就是调用成员函数修改数据的实现
清晰具体的过程吧:
S1.Push(1);
底层汇编是 lea rcx, [S1] 意思是取S1的地址给寄存器 rcx
等到了(公共代码区的)成员函数, 寄存器 rcx 将寄存的 S1的地址 给到 this指针,要使用S1该对象里的数据时,是通过this指针来实现的
用刚刚的栈举栗子:
我们一般在成员函数内部直接写:_capacity = Init_SZ;
这样可以访问到 S1中的 _capacity,是因为底层是这样的:this->_capacity = Init_SZ;
是通过this指针来访问到 S1 中的数据;这里又一次体现出 C嘎嘎更多是让编译器帮我们完成了操作,而不是实质性地改变C语言
是成员函数就有 this 指针(这是属于每一个成员函数的浪漫)