当前位置:   article > 正文

C++——类和对象初阶讲解完整版(类的引入、类的定义、类的作用域、类的实例化、类访问限定符及封装、类成员函数的this指针、析构函数、构造函数、拷贝构造函数、赋值操作符重载、类的静态成员......)_c++中在两个不同地方实例化类,那么这两个实例化的变量会共享吗?

c++中在两个不同地方实例化类,那么这两个实例化的变量会共享吗?

在这里插入图片描述

在这里插入图片描述

C++——类和对象讲解总览

在这里插入图片描述

本篇文章的讲解将按照上述的方式,逐层递进,分分钟帮你拿捏C++类和对象这部分。

类和对象上(基础篇)

在这里插入图片描述

1、初识面向对象编程:

初步了解面向过程和面向对象的区别

在这里插入图片描述

举个比较感人的例子用来帮助铁子们理解(友情提示:内容可能引发不适,不能接受请及时略过)

在这里插入图片描述

狗改不了吃屎,这其中的狗吃屎(狗选择了什么食物来填饱肚子)就可以理解为面向对象

在这里插入图片描述

而吃狗屎,(狗吃屎的过程)狗享受食物的这个过程就可以理解为面向过程

在这里插入图片描述

狗的食物需求种类丰富多样,可以是大米饭,也可以是猪排骨,还可以是屎…在这些用来填饱肚子的食物当中,狗对自己的便便算得上是情有独钟了,毕竟吃起来回味无穷,这样看起来如此秀色可餐的美食也让我们每个人都垂涎三尺了,但是很可惜的是我们无福消受。

在这里插入图片描述

2、类的引入:

在这里插入图片描述

我以定义菜单上的食物类为例,向大家讲解他们之间的区别:

(1)C语言的方式

在这里插入图片描述

(2)C++的方式

在这里插入图片描述

从上述的例子,我们就能轻而易举地看出其区别,C语言只允许有成员变量在结构体中,而C++的结构体中不仅仅只有成员变量,而且还允许有成员方法(即所谓的函数),也就是C++将结构体升级成为了类,所以更喜欢用class来表示类,而不是struct。(注:class在英文翻译中不单单是只有班级的意思,更还有类这方面的意思)

在这里插入图片描述

3、类的定义:

(1)模板样式

在这里插入图片描述

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面是有分号的,千万不要忘记,有的编译器会帮你自动补充,但是也有不会帮你自动补充的!

类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。

(2)类的两种定义方式:

以我前面写的食物类为例:

【1】 声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。

在这里插入图片描述

扩展迁移(何为内联函数?懂得小伙伴可以略过)

很多小伙伴可能不太清楚内联函数是什么意思,那这里我再简单讲解下内联函数:

① 概念:以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率。

② 特性:

a. inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。

b. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等
等,编译器优化时会忽略掉内联。

c. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会
找不到。

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

在这里插入图片描述

在这里插入图片描述

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

4、类的访问限定符及封装:

(1)访问限定符

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

在这里插入图片描述

【访问限定符说明】

① public修饰的成员在类外可以直接被访问

② protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)

③ 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止

class的默认访问权限为private,struct为public(因为struct要兼容C)

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

拓展引申:

基于第④小点,我们就可以明确地清楚C++中struct和class的主要区别是什么?

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

(2)封装

在类和对象阶段,我们只研究类的封装特性,那什么是封装呢?

概念:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

封装本质上是一种管理

就比如说当下全球都受到新冠疫情的影响,在疫情初期,我国作为世界上少有的大国,又是如何做到疫情防控楷模,有效地抑制疫情在中国的传播呢?我们使用健康码通行制度(红码、黄码、绿码),只有当你是绿码的时候,你才能自由通行去想去的地方,否则就要进行隔离,并且当有地方出现疫情以后,也会根据疫情的严重程度变为中高风险区、高风险区等等,只有当连续14天都没有出现疫情才降为低风险区,正是因为国家这么合理有效地管控疫情,我国疫情才能如此的平稳。很难想象如果我国不采用这些制度来管控疫情,那么我国疫情的现状又该如何?说到这里,我们就得大大的为国家点赞了,非常庆幸生在中国,感谢祖国的保护。

在这里插入图片描述

类的封装(使用protected/private把成员封装起来)也是一样的道理,好比你这边是疫情高风险区,就必须得封控起来不让你出去,也不能让外面进来,进而才能有效地抑制疫情的发展。如果你类的封装不够完善,安全性不高,就如同这疫情防控一样,岂不是乱套了吗?

5、类的作用域:

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

以我前面写的食物类为例:

在这里插入图片描述

在这里插入图片描述

6、类的实例化

(1)简介:

概念:用类类型创建对象的过程,称为类的实例化。

① 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。

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

③ 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什
么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占
用物理空间。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

以我前面写的食物类为例:

在这里插入图片描述

7、类对象模型:

(1)如何计算类对象的大小?
在这里插入图片描述
问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小呢?

在这里插入图片描述

实践出真知,我们对类对象在编译器中的存储方式进行分情况讨论:

情况1:类中既有成员变量,又有成员函数:。

情况2:类中只有成员函数。

情况3:类中什么都没有,即空类。

以下列测试代码为例:

在这里插入图片描述

通过计算我们可以得知,3种情况下的类的大小分别为4、1、1

在这里插入图片描述

发现一个很奇怪的现象就是在情况2和情况3下,类的大小算出来的结果都是1,why?这是因为编译器在类对象的存储过程中,将类中的成员函数放到了公共的代码段上面,这也就意味着一个类的大小计算方式本质上和C语言结构体大小的计算方式一样,就是该类中“成员变量”之和,成员函数并不参与计算。并且编译器规定空类(即既没有成员变量又没有成员函数的类)默认占1一个字节,代表空类的存在。

为什么编译器要把类对象的成员函数放到公共的代码段上面呢?

主要原因在于:比如我们类实例化出来的每个对象中的成员变量是不同的,但是需要调用同一份函数,假设不将类对象中的成员函数放到公共的代码段上面,那么当一个类创建多个对象时,每个对象中都会保存一份这样的代码,相同代码被保存多次,相当地浪费存储空间,这是非常愚蠢的做法。

因此大佬们才将类对象的成员函数规定放在公共的代码段上面,极大地节省存储空间。

关于C语言结构体内存对齐规则,若有遗忘的小伙伴可以参考我下面链接的这篇博客:

附:结构体内存对齐规则

8、this指针:

(1)this指针的引出:以定义一个学生成绩类为例

在这里插入图片描述

在这里插入图片描述

针对该类,存在着这样的一个问题,就是StudentScore类中存在有Show和SetScore两个成员函数,函数体中并没有关于不同对象的区分,假设当s1调用SetScore函数时,该函数是如何知道应该设置s1对象,而不是设置s2对象呢?

为此,C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

(2)this指针的特性:

① this指针的类型:类类型* const。

② 只能在“成员函数”的内部使用。

③ this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this
形参。所以对象中不存储this指针。

④ this指针是成员函数第一个隐含的指针形参,一般情况下由编译器通过ecx寄存器自动传递,不需要用户
传递。

前面定义的一个学生类实际上就被编译器处理成了下面的这个样子,以此类推。

在这里插入图片描述

在这里插入图片描述

简而言之,切记就是:

① 调用成员函数时,不能显示传参给this。

② 定义成员函数时,也不能显示声明形参this。

③ 在成员函数内部时,我们可以显示使用this。(通常是在特殊情况下才会显示使用this,一般情况下我们都不会显示使用this)

在这里插入图片描述

类和对象中(详解篇)

在这里插入图片描述

1、类的六个默认成员函数

假设一个类中什么成员都没有,我们就简称为空类。但是空类中实际上就真的什么都没有吗?然而并不是的,任何一个类在我们不写的情况下,都会自动生成下面六个默认成员函数。

在这里插入图片描述

在这里插入图片描述

2、构造函数

(1)概念:构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只调用一次。

(2)特性

需要注意的是,构造函数是特殊的成员函数,虽然名称叫做构造函数,但是它并不如其名,它的主要作用是初始化对象,而不是开空间创建对象。

① 函数名与类名相同。

② 无返回值。

③ 对象实例化时编译器自动调用对应的构造函数。

④ 构造函数可以重载。

以日期类为例(下同),向大家演示讲解:

在这里插入图片描述

在这里插入图片描述

⑤ 如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成。

在这里插入图片描述

⑥ 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的默认构造函数,都可以认为是默认成员函数。

在这里插入图片描述

在这里插入图片描述

编译时,我们会发现两个错误和一个警告。实际上在这段代码中无参的构造函数和全缺省的构造函数在语法上是完全正确,重载是成立的。但是编译器在调用过程中,会存在二义性,编译器无法识别这个无参的对象初始化到底应该调用哪一个构造函数。

在这里插入图片描述

但是如果我们不是无参对象初始化,编译器是能完全正确编译出来的。因此,在我们的代码实践中一定要注意避免这样类似的坑,防止代码出现二义性这种现象。

在这里插入图片描述

⑦ C++里面把类型分为两类:内置类型(基本类型)和自定义类型。内置类型为int/char/double/指针/内置类型数组等等,自定义类型为struct/class定义的类型等等。我们不写编译器默认生成构造函数,对于内置类型的成员变量不做初始化处理,对于自定义类型的成员变量会去调用它的默认构造函数(不用参数就可以调的)初始化,如果没有默认构造函数编译器就会报错

在这里插入图片描述

3、析构函数

(1)概念:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象的资源清理工作。

(2)特性:

① 析构函数名是在类名前加上字符~。

② 无参数无返回值。

③ 一个类有且只有一个析构函数。若未显示定义,系统会自动生成默认的析构函数。

④ 对象生命周期结束时,编译器会自动调用析构函数。

在这里插入图片描述

日期类这样的通常是不需要我们自己写析构函数,但是类似于栈这样需要从堆申请开辟空间的就需要我们自己写析构函数,否则我们编译器自己是不能完成对象的资源清理工作。

⑤ 如果我们不写默认生成的析构函数和构造函数类似,都是对内置类型的成员变量不做处理,对于自定义类型的成员变量会去调用它的析构函数。

在这里插入图片描述

默认生成的构造函数和析构函数会对自定义类型成员变量调用它的构造和析构,在某种程度上也是起着一定的作用,并不是一无是处,切勿以偏概全。

在这里插入图片描述

4、拷贝构造函数

在这里插入图片描述

(1)概念:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

(2)特性:

① 拷贝构造函数是构造函数的一个重载形式。

② 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。(调用拷贝构造,需要先传参数,传值传参又是一个拷贝构造,然后呢调用拷贝构造,又需要先传参数,传值传参又是一个拷贝构造,反复死循环)

在这里插入图片描述

在这里插入图片描述

③ 若未显示定义,系统生成默认的拷贝构造函数。(其作用为对于内置类型成员,会完成按字节序的浅拷贝;对于自定义类型成员,会调用它的拷贝构造函数)

所谓的浅拷贝就是默认的拷贝构造函数构造对象时按内存存储并按字节序完成的拷贝,与之相对应的为深拷贝(通常需要我们自己写的拷贝构造函数),类似日期类这样的拷贝构造函数不需要自己写也行。

在这里插入图片描述

但是类似栈这样的类就需要我们自己写拷贝构造函数,若是依靠系统生成的默认拷贝构造函数就会出现下面这样子的报错,程序直接就崩溃了,原因在于他们指向的同一块空间被析构了两次,因此为了避免这样类似的情况发生,我们自己需要谨记写好拷贝构造函数。

在这里插入图片描述

总结:关于拷贝构造函数,我们不写生成的默认拷贝构造函数对于内置类型和自定义类型都会拷贝处理。但是处理细节是不同的,这个跟析构和构造函数有所差异。

在这里插入图片描述

5、赋值运算符重载

(1)简介:

① C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

② 函数名字为:关键字operator后面接需要重载的运算符符号。

③ 函数原型:返回值类型operator操作符(参数列表)。

注意切记:

【1】不能通过连接其他符号来创建新的操作符:比如operator@。

【2】重载操作符必须有一个类类型或者枚举类型的操作数。

【3】用于内置类型的操作符,其含义不能改变,例如:内置的整形+,不能改变其含义。

【4】作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参。

【5】.*、::、sizeof、?:、.这5个操作符是不能用来重载的。

在这里插入图片描述

对比分析下拷贝构造和赋值运算符重载比较显著的区别,比如说拷贝构造是为了创建新对象能在原有的对象下进行复制,而赋值运算符重载是原有的对象之间的操作。

在这里插入图片描述
同时,当我们在写赋值运算符重载这些函数时,若懂得在合适的情况下使用引用返回,可以有效地减少拷贝构造函数的调用。

a、使用引用返回

在这里插入图片描述

b、没有使用引用返回

在这里插入图片描述
赋值运算符主要有如下五点需要记住:

① 参数类型

② 返回值

③ 检测是否自己给自己赋值

④ 返回*this

⑤ 一个类如果没有显示定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。(即浅拷贝)

编译器默认生成赋值重载函数,跟上述的拷贝构造函数做的事情基本类似,对内置类型成员,会完成字节序的值拷贝(即浅拷贝),对于自定义类型成员变量,会调用它的赋值重载函数,这里我就不过多重复叙述。

在这里插入图片描述

6、日期类的具体实现深入了解类和对象

一般我们写具体的项目的时候,都是类似将头文件、源文件、测试文件各部分功能分开写,这样子条理更加清晰,也便于找到错误。头文件将类所需要的库文件、类的方法声明这类的写好,而源文件就是将这些类方法定义实现好,测试文件就是用于验真这些方法的正确性。

日期类项目实现的功能大致划分为显示日期,两个日期的大小比较,推算日期(类似日期+天数,推算出新日期这样的),两个日期相差多少天,以及最后推算出当前日期是星期几。

Date.h 头文件的实现
在这里插入图片描述

Date.cpp 源文件的实现

① 类的构造函数实现:默认用户不初始化的日期都初始化为公元元年(1年1月1日),同时若用户初始化了日期,要将其判断是否为合法日期,防止出现非法日期,这又引申出了如何判断用户的日期是否合法的问题?很简单,我们只需要再创建一个方法(根据年月份推理出天数),如果天数不合理就是非法日期,反之就是合法日期。日期类的打印函数就相对简单,没什么可说的。

在这里插入图片描述

② 日期的大小比较函数,两个日期之间的大小关系有大于、小于、等于、大于等于、小于等于以及不等于,我们只需要具体实现大于或者小于配合等于的实现就能代码复用出其他的关系比较。

在这里插入图片描述
我实现的是通过大于和等于的方法来代码复用出其他的比较关系。

在这里插入图片描述

③ 推算日期(类似日期 + 天数,推算出新日期这样的)函数的实现。通过实现日期 += 天数的函数可以代码复用出 + 的函数实现, - = 也是同理复用出 - 的函数实现,通过前置 ++ 的实现,可以代码复用出后置 ++ 的实现,- - 也是同理。

在这里插入图片描述

日期 += 函数的实现思想就是天数就够就往月数进位,月数够了就往年数进位,以此类推,日期 + 的函数直接代码复用 += 的函数实现。

日期- = 函数的实现同理,天数不够减了就往月数去借,月也不够了就往年去借,日期 - 的实现同样是代码复用- = 函数。

需要注意出现 += 或者 - = 负数的情况,实际上就是 += 变为 - = ,然后 - = 变为 += 而已。稍加判断切换调用函数即可。eg1:2023年1月5日 += -34 天,也就是 2023年1月5日 - = 34 天;eg2:2023年2月13日 - = -66 天,也就是2023年2月13日 += 66 天。
在这里插入图片描述
在C++的标准规定中,为了区分前置 ++ 和后置 ++ 运算操作符重载函数的区别,引入了占位参数的概念,即后置 ++ 中会默认有一个规定为int类型的占位参数,便于编译器调用时进行区分前置还是后置,我们在使用时是不需要管这个默认占位参数的,编译器会自动帮助填充调用,但是在写方法的时候需要注明默认int类型的占位参数。

在这里插入图片描述

日期前置 ++ 的实现非常简单,在当前日期往后+一天用引用返回去即可;后置 ++ 的实现直接可以代码复用前置 ++ ,通过拷贝构造的方式初始化一个跟当前日期一样的日期,然后再将当前日期 += 1,返回之前拷贝构造的变量即可。前置 - - 和后置 - - 也是同理。

在这里插入图片描述

④ 两个日期相差多少天的函数实现。

我实现的思想是:默认两个日期相减时,第一个日期是大于第二个日期的,利用一个循环以小的日期为基准,看还差多少天才等于大的日期。这样子就可以算出来两个日期相差多少天了。

前面有提到我默认第一个日期是大于第二个日期的,那么当出现第一个日期小于第二个日期的情况怎么办呢?很简单,采用一个标记变量初始值为1,若第一个日期小于第二个日期就变为 - 1,然后对换两个日期的位置,这样子最大的日期就能始终默认是第一个日期了。最后在返回值做手脚(根据标记变量返回正负天数),若起初第一个日期就大于第二个日期,标记变量就是1,返回正数;否则标记变量为 - 1 返回负数即可。

在这里插入图片描述

⑤ 推算出当前日期是星期几的函数实现。

思想同样是以指定的一个标准日期来计算星期,我默认的是1900年的1月1日,这个时间对应的星期刚好是星期一。

之所以以1900年1月1日为标准是因为在网上查了很多关于日期星期的查询器,发现划到最早的只能看到1900年的1月1日,所以以此作为标准。

那么后续代码实现的操作就非常容易了。定义一个日期数组,元素是星期一到星期天。然后用当前日期减去标准日期的结果取余7(这样结果就只能是0到6),根据这个结果依次对应是星期几。

楼主实现的星期查询是存在缺陷的,就是只能查询出1900年1月1日以后的日期,在这之前的日期是无法准确查询出来的。

在这里插入图片描述

Test.cpp 源文件的实现

这个源文件主要是用来验证前面类中的各方法是否成功实现,在实践写代码的过程中,最好就是边写边测试,不要一次性写完再慢慢测试找错误,这样你会压根很难发现问题。

① 验证显示日期的方法是否正确。
在这里插入图片描述

② 验证日期的大小比较的方法是否正确。

在这里插入图片描述

③ 验证推理日期的方法是否正确。

④ 验证两个日期相差多少天的方法是否正确。

在这里插入图片描述

⑤ 验证当前日期是星期几的方法是否正确。

在这里插入图片描述

附1:C++日期类模拟实现的源码提取

Date.h

#pragma once // 防止多个头文件被重复包含

#include <iostream>

using namespace std;

class Date
{
   
public:
	Date(int year = 1, int month = 1, int day = 1); // 构造函数 —— 日期的初始化

	// 日期类的拷贝构造函数,可以不写
	/*Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;

		cout << "Date(const Date& d)" << endl;
	}*/

	int AcquireMonthDay(int year, int month); // 获取月份天数函数 —— 根据年份月份,推算出合理月份的天数,免得出现非法日期

	void Print(); // 打印函数 —— 打印日期

	// 实现 > 或 < 和 == 其他的就可以完成代码的复用
	// 不仅仅是Date类可以这样子,其它类要实现这样类似的比较,也可以以此类推

	bool operator>(const Date& d); // 日期的大小比较函数 —— 先年比,后月比,再天数对比,下同理

	bool operator==(const Date& d);

	bool operator>=(const Date& d);

	bool operator<(const Date& d);

	bool operator<=(const Date& d);

	bool operator!=(const Date& d);


	Date& operator+=(int day); // 推算日期函数 —— 日期+天数,推算出新的日期,下同理

	Date operator+(int day
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签
  

闽ICP备14008679号