《类和对象(上)》
引言:
上次我们学习了C++的一些入门基础,但其实还没有入门C++,想要入门C++,肯定是要把类和对象这部分学透彻,这次先来学习类和对象(上)
一:类的定义
1. 类定义格式:
- class为定义类的关键字,- Stack为类的名字,- {}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量,类中的函数称为类的方法或者成员函数。
- 为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加_或者m开头,注意C++中这个并不是强制的,只是一些惯例,具体看公司的要求。
- C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是struct中可以定义函数,一般情况下我们还是推荐用class定义类。
- 定义在类里面的成员函数默认为inline。
场景一 :类和结构体的对比
这个是我们用类来实现的栈(功能并不完善,重在对比)
 
 
这是我们之前用结构体实现的:
 
 
 通过对比可以发现几点区别:
- 用结构体来实现栈时,数据和方法是分开的;用类来实现栈的话,数据和方法是放在一块的。
- 用结构体来实现栈时,为了避免函数名冲突,还需要有一些前缀来进行区分,比如SKPush,但是如果用类来实现栈的话,就不用再顾虑这个点,因为不同的类域里面允许存在相同的函数名。
场景二:特殊标识来区分成员变量
 来看上面这个场景,初始化时传入的形参和成员变量的名字相同,这时候就很难去区分了,为此,按照惯例的话会将成员变量加上一些特殊标识,如下面这种情形:
来看上面这个场景,初始化时传入的形参和成员变量的名字相同,这时候就很难去区分了,为此,按照惯例的话会将成员变量加上一些特殊标识,如下面这种情形:

 在成员变量中加上了一个前缀_来进行区分,这个按照个人习惯即可,并没有什么硬性要求。
场景三:C++中的结构体
尽管在C++中引入了类,C++中也是可以兼容struct的用法的,并且struct在C++中也升级为了类,比较明显的就是在struct中也可以定义函数了,尽管如此,我们还是建议使用class来定义类。
 
 由于struct升级为了类,因此无需再typedef ,struct的名字就可以用来表示类型。
 
2. 访问限定符:
访问限定符分类

- C++用一种实现封装的方式,将类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
- public修饰的成员在类外可以直接被访问;- protected和- private修饰的成员在类外不能直接被访问,- protected和- private是一样的,以后继承章节才能体现出他们的区别。
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到}即类结束。
- class定义成员没有被访问限定符修饰时默认为- private,- struct默认为- public。
- 一般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public。
场景一:public的成员和private的成员对比

 由于Init函数是在public里面,可以访问,但是top是在private里面,不能访问,所以编译器在这里会报错。
场景二:class和struct定义类-默认限定符对比

解读:
由于class定义类时,默认访问限定符为private,因此这里都不能访问了。

解读:
由于struct定义类时,默认访问限定符为public,所以这里都可以访问。
3. 类域:
- 类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要用::作用域操作符指明成员属于哪个类域。
- 类域影响的是编译的查找规则,下面程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到arr等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的arr等成员,就会到类域中去查找。
在类中函数定义和声明分离时的注意事项

 解决方法-指定类域
 
引申思考:
既然声明和定义分离容易出错,那么我们为什么还要这么做呢?其实这样做还是有好处的:
- 避免链接时的冲突
- 将一些复杂函数进行声明和定义的分离,增加代码的可读性。
二:实例化
1.实例化概念:
- 用类类型在物理内存中创建对象的过程,称为类实例化出对象。
- 类是对象进行一种抽象描述,是一个像模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
- 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。比如:建造房屋时,首先有一个设计图纸来规定这个房屋的大小、风格、构造等,并没有真的建造出一个实体。类就像这个房屋图纸一样,只是规定了类中都有哪些成员,并没有为其分配空间。

实例化举例


2. 对象大小
分析一下类对象中都有哪些成员呢?类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?首先函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。再分析一下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各自独立的成员_year_month_day存储各自的数据,但是d1和d2的成员函数Init/Print指针却是一样的,存储在对象中就浪费了。如果用Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。其实函数指针是不会存储的,但是现在还没学,解释不了这个问题。
因此得出结论:
 对象中只会存储成员变量,还需要注意这里也要遵守内存对齐原则
旧知复习:
内存对齐规则:
- 第⼀个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
- VS中默认的对齐数为8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小。
练习:
计算A B C 在内存中的空间大小
 
分析:

 但是B和C里面都没有成员变量,难道B和C是0吗?
 我们来打印看一下

解读:
这里可以看到a占用的空间大小和我们分析的一样,但是b和c都是1,并不是0,这是为什么呢?
因为如果一个字节都不给,怎么表示对象存在过呢!所以这里给1字节,纯粹是为了占位标识对象存在。
三:this 指针
Date类中有Init与Print两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这里就要看到C++给了一个隐含的this指针解决这里的问题。
其实编译器在编译后,类的成员函数默认都会在形参第一个位置,增加一个当前类类型的指针,叫做this指针。比如Date类的Init的真实原型为,void Init(Date* const this, int year, int month, int day)
类的成员函数中访问成员变量,本质都是通过this指针访问的,比如这里在进行初始化时:this-> _year = year
 注意:C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针。
 
 这里在调用函数的时候其实自动将d1和d2的地址传了过去this指针接收了,因此函数才能区分d1和d2。
 在类里面是可以将this指针写出来的,但不能在参数中写。
 
 在参数里面写是会出错的,因为这是编译器的活儿,咱们不用多管闲事。
 
完结
撒花!!!
