c++ - 第2节 - 类与对象(上)

本文介绍了C++中的面向对象编程基础,包括面向过程与面向对象的区别、类的引入和定义、访问限定符与封装、类的作用域、实例化以及对象模型。重点讲解了类的封装特性,强调了成员函数和成员变量的访问控制,并通过实例解释了this指针的作用。此外,还探讨了类对象在内存中的存储方式以及this指针的使用场景。
摘要由CSDN通过智能技术生成

1.面向过程和面向对象初步认识

C语言是 面向过程的, 关注的是 过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是 基于面向对象的, 关注的是 对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

2.类的引入

c++兼容了c语言中struct的用法,同时对struct进行了升级,把struct升级成了类

升级成类的两个标志:

(1)结构体名称可以做类型

(2)结构体里面可以定义函数

注:

1.访问结构体里面的函数,与访问结构体成员类似,形式为:结构体变量名 . 函数名

2.如下图所示,c++结构体成员变量前不是必须加 _ (c++没有规定),但是习惯加这个 _ ,用来标识成员变量

3..c++中一般不再对结构体进行重命名,因为c++中结构体名称可以做类型了,但是如果结构体名称太长,也可以typedef,c++中对结构体重命名有两种方式,一种是以前c语言的方式,另一种直接对结构体名称类型重定义(因为结构体名称可以做类型了)

4.c++的类是一个整体,也就是说在类里面不是向上查找,而是整个类里面查找,因此成员变量定义不一定非要在成员变量使用的前面

5.上面结构体的定义,在C++中更喜欢用class来代替,其中class是类,用类定义出来的变量我们叫做对象


3.类的定义

class className
{
 // 类体:由 成员函数 和 成员变量 组成
 
}; 
class为定义类的 关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面 分号
类中的元素称为 类的成员:类中的 数据称为 类的属性或者 成员变量; 类中的 函数称为 类的方法或者 成员函数
类的两种定义方式:
1. 声明和定义全部放在类体中

2. 声明放在.h文件中,类的定义放在.cpp文件中

一般情况下,更期望采用第二种方式 

注:

1.在class类中使用访问限定符public,public是将class类中public后面(到下一个访问限定符或大括号为止)定义的成员函数和成员变量设置成公有的,如果不设置公有,那么class默认为私有,私有的话在类的外面(包括主函数)无法调用访问。

struct中成员函数和成员变量默认为公有的,如果想设置成私有,可以用访问限定符private修饰

2.在类体外定义成员,需要使用 ::(作用域解析符)指明成员属于哪个类域。

这里使用作用域解析符指明了类域,那么定义的部分实际上也属于类域的一部分,所以在定义部分中同样可以使用设置为私有的成员变量(访问限制符private限制的是从类域外面进行访问)

3.成员函数如果在类中定义,编译器可能会将其当成内联函数处理。这就告诉我们,在实际中,一般情况下短小函数可以直接在类里面定义,长一点函数声明和定义分离


4.类的访问限定符及封装

4.1.访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

访问限定符说明:
1. public修饰的成员在类外可以直接被访问
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符就到 }结束
4. class的默认访问权限为private,struct为public(因为struct要兼容C)

注:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

问题:C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是的成员默认访问方式是private。

4.2.封装

面向对象的三大特性: 封装、继承、多态
在类和对象阶段,我们只研究类的 封装特性
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理:我们如何管理兵马俑呢?比如如果什么都不管,兵马俑就被随意破坏了。那么我们首先建了一座房子把兵马俑给封装起来。但是我们目的不是全封装起来不让别人看。所以我们开放了售票通道,可以买票突破封装在合理的监管机制下进去参观。类也是一样,我们使用类数据和方法都封装到一起。不想给别人看到的,我们使用protected/private把成员封装起来。开放一些共有的成员函数对成员合理的访问。所以封装本质是一种管理(封装是一种更严格的管理方式)。

c语言的数据和方法是分离的,这样太过自由,是不太好管理的,如下图所示。如果我们想打印栈顶元素,我们可以使用printf("%d\n", StackTop(&st)),这是正常的写法,但是c语言里面也可以使用printf("%d\n", st._a[st._top])直接访问栈顶元素进行打印,这样打印其实是有问题的,如果StackInit初始化ps->_top为0的话,那么此时打印的是最后一个数据的下一个,如果StackInit初始化ps->_top为-1的话,这里打印才是最后一个元素,也就是栈顶元素。

因此c语言这样太过自由,容易出错,需要进行封装来管理限制

在c++中,如下图所示,我们可以直接定义一个类,类名为stack,与类相关的函数可以直接放在class类里面变成成员函数,函数名中的stack也可以删除,因为这些函数已经在stack类里面了,并且成员函数形参不再需要像之前一样传一个结构体变量地址了,因为每个定义的类变量里面都有这种功能的成员函数,直接调用每个类变量里面的成员函数即可。在类里面不能在外进行访问的设置为私有,可以在外面访问的设置为公有。

这样就将数据和方法封装在了一起,都封装在了类里面。想给你自由访问的设计成公有,不想给你直接访问的设计成私有,有效的进行了管理(这样做,在外面就没办法使用成员变量了,失去自由的同时更加规范了,使函数实现和函数功能分开,防止了在不知道函数实现的情况下对成员变量进行误用)(在项目工程中要尽量实现低耦合高内聚,一个人改其他人不至于都要跟着改。在外面不能使用成员变量,使函数实现和函数功能分开,就使代码变得低耦合了,也就是代码和代码之间的关联性降低)

一般情况设计类,成员数据都是私有或者保护,想给访问的函数是公有,不想给访问的函数是私有或者保护


 5.类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中 在类体外定义成员,需要使用 :: (作用域解析符)指明成员属于哪个类域(在前面类的定义中做过说明)

注:

1.两个不同的类域里面有相同的成员函数Push,这两个函数不构成重载(因为这两个函数函数名相同参数类型也相同,并且重载的要求是在同一作用域),但是这样是可行的,原因是他们在不同的作用域,不同的作用域里面可以有同名变量和同名函数。


6.类的实例化

在类的内部定义成员变量的部分,其实只是成员变量的声明,因为此时这些成员变量没有申请占用空间内存(声明和定义的本质区别就是声明不占用内存空间,而定义占用内存空间)当用类创建对象的过程,申请了内存空间,才算是这些成员变量的定义

用类类型创建对象的过程,称为类的实例化
1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
2. 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间


7.类对象模型(类对象的存储方式)

猜测一:对象中包含类的各个成员

缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。因为不同对象的成员变量数值不一样而成员函数是一样的,因此有了下面猜测二

猜测二:只保存成员变量,成员函数存放在公共的代码段

问题:对于上述两种存储方式,那计算机到底是按照那种方式来存储的?

结论:一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐(内存对其规则与结构体完全一样)。
注意:如果是没有成员变量的类对象,编译器给一个字节来唯一标识这个类,表示对象存在。如果空类不给内存空间,那么用这个类定义出的对象就没有地址,这显然不行,如下图所示
st1._top 、 st1.Init() 、 st2._top 、st2.Init() 代码意义是不一样的,遇到st1._top代码,编译器是在st1这个对象里面找,遇到st2._top代码,编译器是在st2这个对象里面找;遇到 st1.Init() 代码和 st2.Init() 代码,编译器都是在公共代码区找,且两个代码调的是同一个函数


8.this指针

8.1.this指针的引出

我们先来定义一个日期类Date

对于上述类,有这样的一个问题:
Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,那当s1调用SetDate函数时,该函数是如何知道应该设置s1对象,而不是设置s2对象呢?
C++中通过引入this指针解决该问题,即: C++编译器给每个“非静态的成员函数“增加了一个隐藏的this指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成如下图所示,编译器自动将上面部分转成下面部分

注:我们在写代码的时候,不能直接在实参和形参把this显示的写出来,如下图一所示,这样相当于抢了编译器的工作,编译不通过。

但是只要不在实参和形参把this显示的写出来,我们是可以使用this指针的,如下图二所示。

编译时,系统中的this其实是被const修饰的,与下图一中所示一样,因此我们不能手动修改this指针本身(const修饰的指针可以初始化,但不能修改,初始化this指针的工作编译器自动做了),但是this指针指向的对象我们可以修改,如下图三所示

8.2.练习题

题目1:

答案:正常运行

注:

1.这里一定不是编译错误,就算是空指针的解引用也不会报编译错误,所有空指针和野指针问题都是在程序运行的时候暴露出来的。

2.如果我们有一个对象d1,对象要访问其类成员就用 . 操作符,如果有一个对象d1的指针p,那么p要访问其指向对象的类成员就用->操作符,

使用对象访问其类成员,编译器处理时传递this指针,是将类的地址传过去(前面介绍过),如下图左所示。使用对象的指针访问其类成员,编译器处理时传递this指针,是将对象的指针直接传过去

    

3.如下图所示,p->_a是解引用,而p->Show()不是解引用。p->_a是在指针p指向的空间找_a类成员变量,所以是解引用,如果此时指针p是空指针,则会报错(如果没有报错,那是因为编译器优化过,自动忽视了该行代码)。p->Show()中的Show函数不在指针p指向的空间中,其是在公共代码区中找Show函数,所以不是解引用,如果此时指针p是空指针,不会报错。如下图所示,代码p->Show(),此时编译器会自动将指针p传给this指针,而p是空指针,所以this指针可以是空指针

题目2:

答案:运行崩溃

注:

1.代码p->PrintA()系统自动将指针p赋值给this指针,而此时指针p是空指针,所以this指针是空指针,函数中运行代码count<<_a<<end1时,系统自动替换成了count<<this->_a<<end1,其中this->_a相当于对空指针解引用,因此运行崩溃

题目3:this指针存在哪里?

答案:正常情况下是存在栈中,有些编译器会使用寄存器优化(因为有些地方频繁调用函数,存在寄存器中,寄存器比内存更快)

题目4:this指针可以为空吗?

答案:可以,题目1的注里面讲了,不再赘述

补充:为什么对空指针解引用会报错,而不是空指针解引用就不会报错呢

空指针其实是内存中0x00000000地址,是一个存在的地址,该地址是预留出来的,不存储任何数据,系统运行程序会进行检查,不能对该地址进行访问,如果访问就会报错,因此这里的报错是检查规定的行为

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风张幔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值