深入理解C++系列(02)——类和对象(上)
文章目录上期回顾C 类和对象上一、本章导读一、精简目录二、本节内容总览二、类的定义1. 类定义的基本格式2. 成员变量命名习惯不是 C 语法强制要求3. class 和 struct 的区别三、访问限定符⭐1. 三种访问限定符2. 访问限定符的作用范围3. 封装的初步体现四、类域1. 类会形成新的作用域2. 为什么类外定义要写 Stack::五、实例化⭐1. 类和对象的关系2. 成员变量什么时候开空间3. 一个类可以实例化多个对象六、对象大小⭐⭐1. 对象中到底存什么2. 对象大小遵循内存对齐⭐️⭐️3. 典型对象大小问题4. 空类大小为什么是 1七、this 指针⭐⭐1. 为什么需要 this 指针2. this 指针的本质3. this 指针的几个关键结论结论一不能在形参和实参位置显式写 this结论二可以在函数体内部显式使用 this结论三this 的类型可以理解为 类类型* const4. this 指针存在哪里八、典型选择题结论⭐️⭐️⭐️题 1空指针调用不访问成员变量的成员函数题 2空指针调用访问成员变量的成员函数九、C 和 C 实现 Stack 的对比1. C 语言写法特点2. C 写法特点3. 二者核心区别4. 最重要的变化封装十三、总结复习时可直接看1、类和对象2、封装思想3、对象中真正存储的内容4、 this 指针5、类域6、理解空类大小为什么是 17、理解空指针调用成员函数下期预告结语◆ 博主此生决int分享编程知识深挖底层原理持续原创更新热门专栏深入理解 C系列从语法入门到底层原理系统掌握现代 C算法系列从入门到精通蓝桥杯、ACM、LeetCode 与面试算法全路线快速复习系列知识梳理、查漏补缺考前冲刺必备Java 速通系列已学 C 语言快速上手 Java轻松备战期末考试 本系列知识点前后关联较强建议按照专栏顺序阅读哦本系列主要面向的阅读人群1学完 C 语言准备系统学习 C 的同学2已经学习过 C希望重新梳理知识体系的同学3想深入理解 C 设计思想与底层原理而不仅仅停留在语法层面的同学不止教你会写代码更带你理解代码为什么这样设计---上期回顾上一篇我们主要学习了《初识C》重点掌握了C对比C语言新增了命名空间、函数重载 、引用、inline 内联函数以及nullptr。如果还有印象模糊的地方建议先回顾上一篇内容再继续阅读本文。C 类和对象上一、本章导读一、精简目录一、本章导读二、类的定义三、访问限定符⭐四、类域五、实例化⭐六、对象大小 ⭐⭐七、this 指针⭐⭐八、典型选择题结论 ⭐⭐⭐九、C 和 C 实现 Stack 的对比十、易错点汇总十一、问题自测清单十二、要点总结二、本节内容总览本节主要围绕 C 面向对象入门中的三个基础问题展开类怎么定义类的成员变量、成员函数、访问限定符、类域。类和对象是什么关系类是抽象描述对象是实际占内存的实体。成员函数如何知道自己操作的是哪个对象通过隐藏的this指针。可以用一句话概括类把数据和操作数据的函数封装到一起对象负责存储具体数据成员函数通过this指针找到当前对象的数据。**二、类的定义1. 类定义的基本格式classStack{public:voidInit(intn4){// 成员函数}private:int*_array;size_t _capacity;size_t _top;};注意点class是定义类的关键字。{}内部是类体。类体中的变量叫成员变量属性类体中的函数叫成员函数方法类定义结束后的分号不能省略。2. 成员变量命名习惯不是 C 语法强制要求为了区分普通变量和成员变量常见写法有int_year;intyear_;intm_year;本人常用_xxx风格例如int_year;int_month;int_day;注意这只是编码习惯不是 C 语法强制要求。3.class和struct的区别C 中struct也可以定义类并且可以在结构体中写函数。structListNodeCPP{voidInit(intx){nextnullptr;valx;}ListNodeCPP*next;intval;};重点区别对比项classstruct默认访问权限privatepublic是否能定义成员函数可以可以C 中是否能表示类型可以可以推荐用途更常用于封装类更常用于简单数据结构最重要的区别默认访问权限的不同classA{intx;// 默认 private};structB{intx;// 默认 public};三、访问限定符⭐1. 三种访问限定符public:protected:private:本节重点掌握访问限定符类外能否直接访问当前阶段理解public可以对外提供的接口private不可以内部实现细节protected不可以暂时和private类似继承中再区分2. 访问限定符的作用范围访问限定符从出现的位置开始生效一直到下一个访问限定符出现或者类结束}。例如classDate{public:voidInit(intyear,intmonth,intday);voidPrint();private:int_year;int_month;int_day;};这里Init、Print是public。_year、_month、_day是private。3. 封装的初步体现C 通过访问限定符实现封装一般情况成员变量一般设为private或protected。成员函数中需要给外部使用的接口设为public。这样做的好处防止外部随意修改对象内部数据让对象的状态由成员函数统一管理。例如 Stack 中private:STDataType*_a;size_t _capacity;size_t _top;外部不能直接改_top、_capacity只能通过s.Push(1);s.Pop();s.Top();s.Empty();这就是封装的基础价值。四、类域1. 类会形成新的作用域类定义了一个新的作用域类中所有成员都属于这个类域。如果成员函数在类外定义需要使用作用域限定符::。classStack{public:voidInit(intn4);private:int*_array;size_t _capacity;size_t _top;};voidStack::Init(intn){_array(int*)malloc(sizeof(int)*n);_capacityn;_top0;}2. 为什么类外定义要写Stack::如果写成voidInit(intn){_array...;}编译器会认为这是一个全局函数而不是Stack的成员函数。这样函数内部访问_array、_capacity、_top时编译器在当前作用域找不到这些成员就会报错。所以必须写voidStack::Init(intn)表示Init是Stack类域中的成员函数。五、实例化⭐1. 类和对象的关系核心区别概念含义是否占用对象数据空间类抽象描述、设计图、类型不占具体对象空间对象根据类创建出来的实体占实际物理空间经典类比类像建筑设计图对象像根据设计图建出来的房子。设计图不能住人房子才能住人。2. 成员变量什么时候开空间在类中写成员变量时classDate{private:int_year;int_month;int_day;};这里的_year、_month、_day只是声明对象将来有哪些数据成员。真正开空间是在实例化对象时Date d1;Date d2;此时d1有自己独立的_year/_month/_day。d2也有自己独立的_year/_month/_day。3. 一个类可以实例化多个对象Date d1;Date d2;d1.Init(2024,3,31);d2.Init(2024,7,5);d1和d2共享同一套成员函数代码但拥有各自独立的成员变量。重点对象中存储的是成员变量不存储普通成员函数代码。注释1注释1对象只保存状态成员变量成员函数代码属于类本身统一存放在代码段由 this 指针决定操作哪个对象。形象的讲就是d1、d2 的成员变量存在栈上如果是局部对象new Date 的成员变量存在堆上Print()、Init() 的代码永远只存在一份在代码区。六、对象大小⭐⭐1. 对象中到底存什么类对象中主要存储成员变量普通成员函数不会存储在每个对象中。原因成员函数编译后是指令放在代码段。多个对象调用的是同一份函数代码。如果每个对象都存函数代码或函数指针会造成浪费。例如Date d1;Date d2;d1和d2各自有独立数据但Init、Print函数代码只有一份。2. 对象大小遵循内存对齐⭐️⭐️类对象大小遵循结构体内存对齐规则。内存对齐核心规则第一个成员放在偏移量为0的位置。后续成员要放到自己对齐数整数倍的位置。对齐数 编译器默认对齐数和成员自身大小的较小值。VS 中默认对齐数通常为8。结构体或类的总大小是最大对齐数的整数倍。如果有嵌套结构体嵌套结构体按自己的最大对齐数对齐。3. 典型对象大小问题classA{private:char_ch;int_i;};分析_ch占 1 字节。为了让_i对齐到 4 的整数倍中间会填充 3 字节。_i占 4 字节。总大小为 8 字节。所以sizeof(A)84. 空类大小为什么是 1classB{public:voidPrint(){}};classC{};B没有成员变量只有成员函数。C是完全空类。但是sizeof(B)1sizeof(C)1原因即使没有成员变量也要给对象 1 个字节用来标识这个对象真实存在。注意这个 1 字节不是用来存成员函数的。它只是占位标识。七、this指针⭐⭐1. 为什么需要this指针成员函数代码只有一份但可以被多个对象调用Date d1;Date d2;d1.Init(2024,3,31);d2.Init(2024,7,5);问题是同一个Init函数怎么知道当前应该修改d1还是d2答案编译器会给非静态成员函数隐含传递一个this指针。2.this指针的本质你写的成员函数voidInit(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}编译器大致会理解成voidInit(Date*constthis,intyear,intmonth,intday){this-_yearyear;this-_monthmonth;this-_dayday;}重点this是当前对象的地址。成员函数中访问成员变量本质都是通过this指针访问。this指针由编译器自动传递。3.this指针的几个关键结论结论一不能在形参和实参位置显式写this不能这样写voidInit(Date*constthis,intyear,intmonth,intday);// 错误理解方式不是用户写法调用时也不能这样写d1.Init(d1,2024,3,31);// 错误应该写d1.Init(2024,3,31);结论二可以在函数体内部显式使用thisvoidInit(intyear,intmonth,intday){this-_yearyear;this-_monthmonth;this-_dayday;}结论三this的类型可以理解为类类型* const例如在Date类中Date*constthis含义this是指向当前对象的指针。this本身不能被修改所以不能写thisnullptr;// 错误但可以通过this修改对象成员this-_year2024;4.this指针存在哪里选择题结论this指针**本质是成员函数的一个隐含形参**所以通常存在栈帧中。注意this指针不在对象里面。对象里面存的是成员变量。八、典型选择题结论⭐️⭐️⭐️题 1空指针调用不访问成员变量的成员函数classA{public:voidPrint(){coutA::Print()endl;}private:int_a;};intmain(){A*pnullptr;p-Print();return0;}结论正常运行原因p-Print()会把p作为this指针传给成员函数。函数体内没有访问成员变量也就没有解引用空指针访问对象数据。因此通常可以正常打印。题 2空指针调用访问成员变量的成员函数classA{public:voidPrint(){coutA::Print()endl;cout_aendl;}private:int_a;};intmain(){A*pnullptr;p-Print();return0;}结论运行崩溃原因_a本质是this-_a。此时this nullptr。访问this-_a等价于通过空指针访问对象成员会崩溃。九、C 和 C 实现 Stack 的对比1. C 语言写法特点C 语言中数据和函数是分离的typedefstructStack{STDataType*a;inttop;intcapacity;}ST;voidSTInit(ST*ps);voidSTPush(ST*ps,STDataType x);voidSTPop(ST*ps);使用时要手动传地址ST s;STInit(s);STPush(s,1);STPop(s);2. C 写法特点C 中数据和函数放进类里classStack{public:voidInit(intn4);voidPush(STDataType x);voidPop();boolEmpty();intTop();voidDestroy();private:STDataType*_a;size_t _capacity;size_t _top;};使用时Stack s;s.Init();s.Push(1);s.Pop();3. 二者核心区别对比项C 语言实现C 实现数据和函数分离封装到类中是否手动传对象地址需要s不需要隐藏传this成员变量保护外部可以随意访问结构体成员可以用private限制类型命名常需要typedef类名本身就是类型名默认参数不支持支持如Init(int n 4)4. 最重要的变化封装本节中 C Stack 的底层逻辑和 C 语言差别不大仍然是动态数组top表示栈顶位置容量不足时扩容入栈、出栈、取栈顶。真正重要的是形态变化C 把数据和操作数据的函数绑定到一起并通过访问限定符控制外部访问这是封装的体现。十三、总结复习时可直接看1、类和对象类Class是一种自定义类型可以理解为对象的设计图。对象Object是根据类实例化出来的具体变量。类定义阶段只描述对象应该有哪些成员并不会分配对象空间只有实例化对象时才会为成员变量开辟内存。类负责描述对象负责存储数据。2、封装思想封装就是将数据和操作数据的方法封装到同一个类中并通过访问权限控制对数据的访问。通常遵循以下原则成员变量设为private成员函数作为public接口对象之间只通过接口进行交互而不是直接修改数据3、对象中真正存储的内容对象中主要存储的是普通成员变量对象中不会存储普通成员函数this指针静态成员函数代码成员函数代码只有一份所有对象共享因此无论创建多少个对象都不会重复存储函数代码。4、this指针每个普通成员函数都会有一个隐藏参数this它表示当前调用成员函数的对象地址。编译器会自动传递this因此程序员无需手动传参。成员函数访问成员变量时本质都会被编译器转换成this-_成员变量因此this可以在成员函数内部直接使用不能显式写在参数列表中不属于对象成员也不会占对象空间。5、类域当成员函数在类外定义时必须告诉编译器这个函数属于哪个类。因此需要加类域例如Stack::Init()6、理解空类大小为什么是 1即使类中没有任何成员类的大小也不是0而是1原因是每个对象都必须拥有唯一的地址因此编译器至少分配 1 字节作为占位。7、理解空指针调用成员函数下面代码Stack*psnullptr;ps-Print();并不一定立即崩溃。判断标准只有一个如果函数没有访问成员变量只执行普通代码可能正常运行如果函数访问了成员变量本质会通过this解引用访问对象数据通常会发生运行时崩溃。因此真正导致崩溃的不是调用成员函数而是解引用空的this指针。本节重点必须真正理解类、对象、封装、成员函数、类域以及this指针的本质。这些是后续学习构造函数、析构函数、拷贝构造、运算符重载、继承、多态等内容时反复要用到的东西下期预告类和对象中结语本文到此结束感谢大家的阅读如果觉得本文对你有所帮助欢迎点赞、收藏、关注也欢迎在评论区一起交流讨论。也欢迎订阅我的深入理解 C系列专栏、快速复习系列专栏 、算法系列专栏。愿每一次敲下键盘都比昨天更进一步愿每一份坚持都终将有所收获