【学习篇】第17期 C++入门必看——类和对象全站最详篇

【你奶奶都能听懂的C++】第17期 C++入门比看------类和对象全站最详篇

开头:

ok了,这真的是好久不见了,时隔2个多月,本人终于来更新博客了(嘻嘻)。前段时间一直在花时间学习算法知识备战蓝桥杯,所以博客的话就放了放,蓝桥杯一结束我就赶来更新我的博客了,以后也会出一期关于学习算法知识的博客,顺便把算法题按照知识分类整理下来,所以记得关注我一起学习进步。当然,祝大家新的一年学业进步,不出bug

今天这一期我们就要正式进入C++部分的学习,首先就是又多又杂又有难度的类和对象部分,这是我们后期学习C++的基石,所以这部分大家要多看多想多去自己敲代码复现,下面就开始我们类和对象的学习

一.类的定义

(1)类的定义格式

如上图所示,class为定义类的关键字,在class后面的Date是类的名字,{}中为类的主体,要注意类定义结束时后面的分号不能省略。类体中的内容称为类的成员,类中的变量称为类的属性或者成员变量;类中的函数成为类的方法或=成员函数

为了区分成员变量,一般我们会在成员变量加一些特殊标志,比如在其前面或后面加 " _ " 或者以 " m " 为开头,但不是一定要做的,只是个人习惯

C++中struct 也可以定义类,C++兼容struct 的用法,同时struct 升级成了类,明显的变化就是在struct中可以定义函数,但是一般来说还是使用class

在类中的成员函数默认是 inline

inline(内联函数) :

将函数代码直接嵌入调用处,代替常规函数调用,消除函数调用开销。

简单来说就是:把函数本体代码直接复制粘贴到每一处调用位置,无跳转、无栈帧

要注意的是:内联函数只是建议,编译器也会自动评估,如果函数体过于庞大,也会忽略内联仍然会建立函数栈帧

(2)访问限定符

public 修饰的成员在类外可以直接被访问,protected 和 private 修饰的成员在类外不能直接被访问,目前来说,protected 和 private 功能是一样的,以后学习到继承再来说它们的区别

访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 } ,即类结束

类中定义成员在没有访问限定符修饰时,class默认为 private , 而struct默认为public

现在我们来举个日期类的例子:

(3)类域

类定义了一个新的作用域,类的所有成员都在类的作用域中,在类外定义成员时,要使用 "::" 作用域操作符指明成员属于哪个类域

类的"::" 目前使用在三个地方:

1.在类外定义成员函数

2.类外初始化静态成员变量(在这期后面讲)


3.类内访问被隐藏的全局变量

二.实例化

(1)概念

用类的类型在物理内存中创建对象的过程,称为类实例化出对象

什么意思?

直接看图:

一个类可以实例化出多个对象,实例化出的对象占用实际的内存空间,存储类的成员变量

(2)对象大小

类的对象只存储成员变量的空间,成员函数不存在于类的对象中

和struct结构体类似,类的对象同样遵循内存对齐规则,详见:结构体+位段+枚举+联合

这里我们用文字回顾一下:

1.第一个成员在与结构体偏移量为0的地址处。

2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

注意:对齐数=编译器默认的一个对齐数与该成员大小的较小值,VS中默认的对齐数为8。

3.结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小

就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

注意:如果类中没有成员变量,其大小为 1 (为了占位)

(成员函数不占空间)

三.this指针

如上图,在类中成员函数不在类中存储,也就是说上面的 Init 和 print 函数,s1 和 s2 调用的是同一个,那么编译器是如何区分不同的对象呢? 这里就涉及到 this 指针

编译器编译后,类的成员函数默认会在形参的第一个位置,增加一个当前类类型的指针,叫做this指针

如上图,类的成员函数中访问成员变量,本质都是通过 this 指针访问的,C++ 规定不能在实参和形参的位置显示的写出 this 指针(编译器自动处理),但是在函数的内部可以显示使用 this 指针

接下来我们来看到题:

这里运行结果是:A.正常运行 B.编译报错 C.运行崩溃

编译报错和运行崩溃的区别:

一开始我们会想 p 作为空指针,p->print() 不是涉及到对空指针的解引用吗,那就是运行崩溃,但结果其实是:正常运行

p->print() 是要去 call 这个函数的地址,但是函数的地址不在类A中

这里仅仅只是将空指针 p 作为参数传给了函数 print , 但是在 print 函数中并不涉及到对 this指针(即空指针p)的解引用操作,但是如果改成这样就是运行崩溃

这就涉及到对空指针的解引用操作了

要注意 this 指针不存放在类中(不然计算类的内存大小的时候要把 this 指针的空间算上了)

四.类的默认成员函数

所谓的默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数,一个类,我们不懈的情况下会默认生成六个默认成员函数,我们重点要学习前4个

(1)构造函数

构造函数的本质是要替代我们以前在stack ,queue 等数据结构的模拟实现中初始化函数 Init 的功能,因为编译器可以自动调用,所以比手动调用要方便的多

要注意的是构造函数的任务不是开空间来创造对象,在实例化的时候空间已经开好了。

首先构造函数的函数名和类名相同,并且没有返回值(也不用写 void ),在对象实例化的时候会自动调用对应的构造函数

如上图,这样我们就不用手动调用 Init 函数,当然,构造函数也可以给缺省值

无参构造函数、全缺省构造函数、不写编译器自动生成的构造函数,这三者都称为默认构造函数

(简单说就是,不需要传参的构造函数就是默认构造函数),但这三者只能有一个,不然会有歧义

除了在构造函数体内对成员变量进行初始化赋值,还可以使用初始化列表

在初始化列表中,每个成员变量只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方,C++11 支持在成员变量声明的位置给缺省值(如上图的 _f ) ,如果在初始化列表中没有显示使用,那么这个缺省值就作为该变量的初始值

引用成员变量,const 成员变量,没有默认构造的类类型变量 ,必须在初始化列表的位置进行初始化

每一个成员变量都会去走初始化列表,如果没有显示在初始化列表初始化的内置类型,是否初始化取决于编译器,如果对于自定义类型,编译器则会报错

初始化列表中按照成员变量在类中声明的顺序进行初始化,跟成员在初始化列表的出现顺序无关

这个程序最终输出的结果是什么?

答案是:_a== 1 _b== 随机值

解析: 成员变量在初始化列表中初始化的顺序取决于声明的先后顺序,上题中 _b 比 _a 先声明,所以在初始化列表中,先初始化 _b ,此时的 _a 未被初始化,而 _a 又是内置类型,所以是随机值 ; 接着就来初始化 _a ,那就是等于 x==1。要注意:在声明中给的缺省值是给未在初始化列表中的成员变量使用的!

(2)析构函数

析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,比如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会自动调用析构函数,完成对象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能

一句话说:析构函数就相当于自动调用 free(delete) 函数,释放堆内存

析构函数是类名前面加上字符 " ~ " ,同样没有返回值(也不用写 void ),一个类只能有一个析构函数(对于一块空间只能释放一次,多次释放会运行崩溃),对象生命周期结束时,自动调用析构函数

(3)拷贝构造

如果一个构造函数的第一个参数是自身类类型的引用,就是用一个已有的对象,去初始化一个新的对象

拷贝构造函数的参数第一个必须是类类型对象的引用 , 不能用传值的方式------>会无穷递归

如上图,如果使用传值传参,在调用拷贝构造的时候,因为形参是实参的一份临时拷贝,s1 作为实参会将自身拷贝到形参 d 上,这个行为本身就是一种拷贝,又会继续调用拷贝构造函数,以此往复,会无穷递归

如果我们没有手动实现拷贝构造函数,编译器会自动生成一个拷贝构造函数,这种自动生成的拷贝构造会对成员完成浅拷贝(也叫值拷贝),本质就是一个字节一个字节的拷贝,但是如果成员变量中存在指针,使用编译器默认生成的拷贝构造函数来初始化另一个对象时,两个对象的指针会指向同一个空间,会出错,并且在析构的时候也会多次释放同一个空间,造成报错

拷贝构造的三种使用场景

1.初始化一个新的对象,如上图

2.将对象作为函数的一个参数

3.作为函数的返回值,返回临时对象,会调用拷贝构造

(4)赋值运算符重载

1.运算符重载

当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义

运算符重载的名字是由 operator和要重载的运算符共同构成,和其他函数一样,同样也具有其返回类型和参数列表以及函数体,例如我们来重载一个日期类的等于号

但这里有一个很大的问题,在类外我们无法访问私有成员变量,解决方法除了后面我们要学习的友元,我们可以把函数放到类里面,由于类里面隐含 this 指针,所以我们可以省略一个参数

运算符重载以后,其优先级和结合性与原先的要保持一致,并且不能重载原来不存在的运算符,例如:operator@

注意下面的运算符不能重载:

目前我们想要打印日期类的年月日是写了一个print函数,那我们来重载一个流输入(>>)和流提取(<<)运算符

2.赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象的拷贝赋值,注意要和拷贝构造区分开,拷贝构造用于一个对象拷贝初始化给另一个要创建的对象

赋值运算符重载是一个运算符重载,规定必须重载为成员函数,并且有返回值,且建议写成类类型的引用,有返回值的目的是为了支持连续的赋值场景


当没有显示的赋值运算符重载时,编译器会自动生成一个默认的赋值运算符重载,会对成员变量进行浅拷贝(值拷贝),也会存在像默认的拷贝构造函数一样的问题,尽量手动实现

小技巧:

如果一个类里面显示实现了析构函数(要手动释放堆内存),那么就要有:

1.拷贝构造函数

2.赋值运算符重载

(5)取地址运算符

1.const成员函数

将const修饰的成员函数称为 const 成员函数,核心作用是:保证该函数不会修改对象的任何非静态成员变量

对于像打印函数 print 这样的,我们并不想去改变其对象本身的值,隐含的 this 指针前的 const 是为了保证 this 指针的指向不会被改变,想要保证指向的对象不被改变,那就要在前面加 const ,为 const Date* const this ,然而C++又规定不能显示写出 this 指针,那么const 成员函数就要把 const 写在括号后面

2.取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,一般这两个函数编译器自动生成的就可以够我们用了,不需要去显示实现。除非一些很特殊的场景,比如我们不想让别人取到当前类对象的地址,就可以自己实现一份,胡乱返回一个地址

五.类型转换

"类型转换"是连接基础类型与自定义类、类与类之间的重要桥梁,本质是"不同类型之间的适配"

如上图,2 是整型这里直接让 s=2,就发生了类型转换,首先生成了一个类型为 A 的临时对象,以2为值进行构造,然后用这个临时对象拷贝构造类 s (编译器一般会优化成直接构造)

当然类型转换也存在隐患,explicit关键字就可以解决这个问题:

explicit用于修饰构造函数或类型转换函数,表示该函数不能用于隐式类型转换,只能在需要显式指定的上下文中被调用。

六.static成员

用 static 修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外面进行初始化

静态成员变量为所有类对象所共享,不属于某个具体对象,不存在对象中,存放在静态区(相当于全局变量)

用 static 修饰的成员函数称为静态成员函数,没有 this 指针,因此静态成员函数不能访问非静态成员

静态对象在声明中不可以给缺省值。因为缺省值是给初始化列表用的,静态成员变量是不走初始化列表的。

我们来看一道有意思的题:

这道题引出了一个知识点------ 静态计数器

静态计数器,是用静态成员变量存储计数结果,搭配构造函数(创建对象时计数+1)、析构函数(销毁对象时计数-1),实现对对象个数的统计,因为静态成员变量属于类,所有对象共享同一份计数,不会因对象创建/销毁而重置。

所以可以利用静态计数器来解决这道题

我们再来一道:

static 局部静态变量是在程序第一次运行到时初始化的
后定义的类先析构

依据这两点,我们可以轻松选出答案:E、B

七.友元

友元提供了一种突破类访问限定符封装的方式

·友元分为: 友元函数友元类

·实现方式为:再函数声明或者类的声明前面加上"friend",并且要把友元声明放到一个类里面
1.友元函数

对于友元函数来说,友元函数可以再类中的任意地方声明,不受访问限定符的限制,一个函数可以是多个类的友元函数,友元函数的目的就是想要访问到一个类的非静态成员

2.友元类

再 A 中添加 B 的友元声明,此时 B 就能去访问 A 中的非静态成员

要注意,此时 A 依然不能去访问 B 的非静态成员 (B是A的好朋友,但是A不是B的好朋友)
友元类是不具有传递性的,如果A是B的友元,B是C的友元,但是A不是B的友元

八.内部类

如果一个类定义在另一个类的内部,这个内部类就叫做内部类。

内部类是一个独立的类,跟定义全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类

内部类默认是外部类的友元类(内部类可以直接访问外部类的所有成员)

九.匿名对象

匿名对象就是一个用构造函数临时创建的没有名字的对象,用完马上销毁,生命周期就只在当前行

匿名对象 ≠ 没有 this,匿名对象只是没有名字,但它依然是一个完整的对象。成员函数是否有 this,只取决于它是不是非静态成员函数,和对象有没有名字完全无关,所以匿名对象依旧调用构造函数和析构函数会开辟空间。

匿名对象的用法就是可以快速的创建一个类对象作为一个 " 值 "

(1)作为函数的参数

(2)直接调用成员函数

(3)作为初始化数据结构

结尾;

好了,这一期C++类和对象全解,制作不易,如果对你有帮助的话,请给我点赞三连,我主页里有更好康的呦。

往期回顾:

1.【学习篇】 第16期 全站最全七大排序超超超详解

2.【实战篇】 第13期 算法竞赛_数据结构超详解(上)

3.【实战篇】 第14期 算法竞赛_数据结构超详解(下)

相关推荐
Sakuyu434682 小时前
C语言基础(一)
c语言·开发语言
码农的神经元2 小时前
2026 MathorCup C 题实战复盘:从高血脂风险预警到 6 个月干预优化的建模思路与 Python 落地
c语言·开发语言·python
土豆~2 小时前
Claude Code源码学习—— Agent Prompt 设计
学习·prompt·claude code
zzzsde2 小时前
【Linux】进程信号(1)理解信号及信号产生的方式
linux·运维·服务器·算法
人道领域2 小时前
【黑马点评日记03】实战:Redis缓存穿透,缓存击穿,缓存雪崩全解析
java·开发语言·jvm·redis·spring·tomcat
阿拉金alakin2 小时前
深入理解 Java 线程池:核心参数、工作流程与常用创建方式
java·开发语言·java-ee
星幻元宇VR2 小时前
VR流动行走平台|让虚拟体验真正“走起来”
科技·学习·安全·vr·虚拟现实
slandarer2 小时前
MATLAB | R2026a 更新了哪些有趣的新东西?
开发语言·数据库·matlab
啊哦呃咦唔鱼2 小时前
LeetCode双指针合集
算法·leetcode·职场和发展