类和对象的基本知识(类的定义,实例化,this指针)
目录
一.类的定义
1.类的定义格式
1.2访问限制符
1.3类域
二.实例化
2.1实例化的概念
2.2对象大小
内存对齐:
三. this指针
const的三种用法:
经典例题:
四、C++和C语言实现Stack对比
一.类的定义
1.类的定义格式
• class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后⾯分号不能省 略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的⽅法或者成员函数
• 为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前⾯或者后⾯加_或者m 开头,注意C++中这个并不是强制的,只是⼀些惯例,具体看公司的要求。
• C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化是 struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类。
• 定义在类⾯的成员函数默认为inline。
1.2访问限制符
• C++⼀种实现封装的⽅式,⽤类将对象的属性与⽅法结合在⼀块,让对象更加完善,通过访问权限 选择性的将其接⼝提供给外部的⽤⼾使⽤。
• public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访 问,protected和private是⼀样的,以后继承章节才能体现出他们的区别。
• 访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有 访问限定符,作⽤域就到}即类结束。
• class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
• ⼀般成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public。
提示:在类里面可以定义多个public和private,但是通常不这样写。
1.3类域
• 类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤::作 ⽤域操作符指明成员属于哪个类域。
• 类域影响的是编译的查找规则,下⾯程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪⾥,就会报错。指定类域Stack,就是知 道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。
二.实例化
2.1实例化的概念
• ⽤类类型在物理内存中创建对象的过程,称为类实例化出对象。
• 类是对象进⾏⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只 是声明,没有分配空间,⽤类实例化出对象时,才会分配空间。
• ⼀个类可以实例化出多个对象,实例化出的对象 占⽤实际的物理空间,存储类成员变量。打个⽐ ⽅:类实例化出对象就像现实中使⽤建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多 少个房间,房间⼤⼩功能等,但是并没有实体的建筑存在,也不能住⼈,⽤设计图修建出房⼦,房 ⼦才能住⼈。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。
2.2对象大小
分析⼀下类对象中哪些成员呢?类实例化出的每个对象,都有独⽴的数据空间,所以对象中肯定包含 成员变量,那么成员函数是否包含呢?⾸先函数被编译后是⼀段指令,对象中没办法存储,这些指令 存储在⼀个单独的区域(代码段),那么对象中⾮要存储的话,只能是成员函数的指针。再分析⼀下,对 象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各⾃独⽴的成员变量 _year/_month/_day存储各⾃的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的,存储在对象 中就浪费了。如果⽤Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这⾥需 要再额外哆嗦⼀下,其实函数指针是不需要存储的,函数指针是⼀个地址,调⽤函数被编译成汇编指 令[call 地址],其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找,只有动态多态是在 运⾏时找,就需要存储函数地址,这个我们以后会讲解。
内存对齐:
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
- 注意:对齐数 = 编译器默认的第一个对齐数与该成员大小的较小值
- VS中默认的对齐数为8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对其参数取最小)的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
#include <iostream> using namespace std; // 普通类:仅含非静态成员变量+成员函数 class Person { public: // 成员变量(占用内存) char gender; // 1字节 int age; // 4字节 short height; // 2字节 // 成员函数(不占用对象内存,仅存代码段) void showInfo() { cout << "性别:" << gender << " 年龄:" << age << endl; } }; int main() { cout << "Person对象大小:" << sizeof(Person) << endl; // 输出? return 0; }| 成员 | 自身大小 | 对齐数(min (自身大小,8)) | 偏移地址 | 占用区间 | 空字节填充 |
|---|---|---|---|---|---|
| gender | 1 | 1 | 0 | 0~0 | 无 |
| age | 4 | 4 | 4 | 4~7 | 1~3(3 字节) |
| height | 2 | 2 | 8 | 8~9 | 无 |
| 整体对齐 | - | 最大对齐数 4 | - | - | 10~11(2 字节) |
注意:静态成员(static)属于「类本身」,而非某个对象,因此不占用对象内存,也不参与对象的内存对齐。
C++ 规定:空类的大小为 1 字节(编译器会插入一个「占位字节」),目的是让空类的对象有唯一的内存地址。
class Empty {}; cout << sizeof(Empty); // 输出1优化技巧:成员变量按 “大到小” 排列,减少填充字节
// 优化前:12字节 class Bad { char a; // 1 int b; // 4 short c; // 2 }; // 优化后:8字节 class Good { int b; // 4 short c; // 2 char a; // 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指针访问的,如Init函数中给_year赋值, >_year = year; this
- • C++规定不能在实参和形参的位置显⽰的写this指针(编译时编译器会处理),但是可以在函数体内显 ⽰使⽤this指针。
这里我们补充一下const的用法:
const的三种用法:
作用:让变量变成只读,不能被修改,从编译期就限制修改,提高程序安全性。
一句话记住:const 一出手,变量不能改;指针分左右,函数最常用。
① const 放在*左边:指向的内容不能改
- 内容不能改
- 指针可以换指向
int a = 10; const int* p = &a; *p = 100; // 报错!内容不能改 p = &b; // 可以!指针能换指向② const 放在 * 右边:指针本身不能改
- 指针不能换指向
- 内容可以改
int* const p = &a; *p = 100; // 可以 p = &b; // 报错!指针不能动③ 两边都 const:啥都不能改
const int* const p;经典例题:
这里为什么不是访问到空指针报错呢?我们来分析一下。
p是一个A*类型的空指针,指向nullptr。- 调用
p->Print()本质是将this指针设为nullptr,并执行Print函数。 Print()是一个非虚成员函数,它的调用是编译期决议(静态绑定),编译器直接根据类型A找到函数地址,不会检查p是否为空。Print()函数内部没有访问任何成员变量(如_a),仅执行了cout输出操作,没有对空指针this进行解引用。- 因此,程序不会触发空指针访问异常,能够正常执行并输出
A::Print()。
p->Print();编译器真正翻译出来的代码是:
Print(p); // 把 p 当作 this 指针传进去!->在这里不是访问内存- 只是告诉编译器:调用
A::Print,并把p当作this传进去 - ✅函数内部没有使用任何成员变量
✅没有解引用 this 指针
✅普通成员函数地址编译期确定,不需要访问对象内存
若要使函数中有成员变量,调用该函数就会出现错误,这里就是空指针访问。
四、C++和C语言实现Stack对比
- C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随机通过对象直接修改数据,这是C++封装的一种体现,这个是最重要的变化。这里的封装的本质是一种更严格规范的管理,避免出现乱访问修改的问题。
- C++中有一些相对方便的语法,比如Init给的缺省参数会方便很多,成员函数每次不需要传对象地址,因为this指针隐含的传递了,方便了很多,使用类型不再需要typedef用类名就很方便
虽然在C++入门阶段实现的Stack看起来变了很多,但是实质上变化不大,后面STL的文章中用适配器实现的Stack,方可感受C++的魅力
