C++中既重要又困难的部分—类和对象

发布于:2025-07-29 ⋅ 阅读:(15) ⋅ 点赞:(0)

今天也是开始学习类与对象啦~~也是个难啃的骨头😭😭这一块可以说是之后内容的基石,务必要好好学习哦!!!

(1)类的定义

1、类定义格式

  • class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后⾯分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数
  • 为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前面或者后面加 _ 或者 m 开头,注意C++中这个并不是强制的,只是⼀些惯例,具体看公司的要求
  • C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是 struct中可以定义函数,⼀般情况下我们还是推荐用class定义类
  • 定义在类面的成员函数默认为inline
  • 类域解决的是类之间的命名冲突

Attention:

  1. 创立类的最后{}一定要加上;(分号)
  2. 为了区分成员变量,⼀般习惯上成员变量,会加⼀个特殊标识,如_ 或者 m(意为my我的函数)开头
  3. C++升级struct升级成了类//类里面可以定义函数 //struct名称就可以代表类型//C++兼容C中struct的用法

2、访问限定符

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

3、类域

  • 类定义了⼀个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域
  • 类域影响的是编译的查找规则,下面程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知 道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找
  • 声明和定义分离,需要指定类域
  • 类里只有成员对象,不存在成员函数的地址

(2)实例化

1、实例化概念

  • 用类类型在物理内存中创建对象的过程,称为类实例化出对象
  • 类是对象进行⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只 是声明,没有分配空间,用类实例化出对象时,才会分配空间
  • ⼀个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。打个比方:类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,设计图规划了有多少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据

 2、对象大小

  • 分析⼀下类对象中哪些成员呢?类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含 成员变量,那么成员函数是否包含呢?首先函数被编译后是⼀段指令,对象中没办法存储,这些指令 存储在⼀个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。再分析⼀下,对 象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各自独立的成员变量 _year/_month/_day存储各自的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的,存储在对象 中就浪费了。如果用Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这里需 要再额外哆嗦⼀下,其实函数指针是不需要存储的,函数指针是⼀个地址,调用函数被编译成汇编指令[call地址],其实编译器在编译链接时,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,就需要存储函数地址,这个我们以后会讲解
  • 上面我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对齐的规则
  • 成员函数有个公共的空间储存,并且没有必要每一次创建函数都把函数储存

3、内存对齐规则 

  • 各位详情可以去下面的链接去看看

C语言复习(10)——自定义类型-CSDN博客

(3)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赋值, this- >_year = year
  • C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针
  • this指针是一个形参

(4)C++和C语言实现Stack对比

面向对象三大特性:封装、继承、多态,下面的对比我们可以初步了解⼀下封装

通过下面两份代码对比,我们发现C++实现Stack形态上还是发生了挺多的变化,底层和逻辑上没啥变化

  • C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的⼀种体现,这个是最重要的变化。这里的封装的本质是⼀种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后面还需要不断的去学习
  • C++中有⼀些相对方便的语法,比如Init给的缺省参数会方便很多,成员函数每次不需要传对象地 址,因为this指针隐含的传递了,方便了很多,使用类型不再需要typedef用类名就很方便
  • 在我们这个C++入门阶段实现的Stack看起来变了很多,但是实质上变化不大。等着我们后面看STL中的用适配器实现的Stack,大家再感受C++的魅力

(5)类的默认成员函数 

默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数(半自动化)。⼀个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最 后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数, 移动构造和移动赋值,这个我们后面再讲解。默认成员函数很重要,也比较复杂,我们要从两个方面去学习:

• 第⼀:我们不写时,编译器默认生成的函数行为是什么,是否满足我们的需求。

• 第⼆:编译器默认生成的函数不满足我们的需求,我们需要自己实现,那么如何自己实现?

1、构造函数 

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并 不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),而是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数自动调用的特点就完美的替代的了Init

<1>构造函数特点

  • 函数名与类名相同
  • 无返回值(返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
  • 对象实例化时系统会自动调用对应的构造函数
  • 构造函数可以重载
  • 如果类中没有显式定义构造函数,则C++编译器会自动生成⼀个无参的默认构造函数,⼀旦用户显式定义编译器将不再生成
  • 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成 函数重载,但是调用时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认生成那个叫默认构造,实际上无参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调用的构造就叫默认构造
  • 我们不写,编译器默认生成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于自定义类型成员变量,要求调用这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要用初始化列表才能解决,初始化列表,我们下个章节再细细讲解

~名词解释 

  • 显示定义:在面向对象编程中,构造函数的显示定义指的是开发者在类中手动编写构造函数代码而非依赖编程语言默认提供的无参构造函数

       1.可以在创建对象时,强制传入必要的参数,确保对象初始化时就具备关键属性(例如,          创建“学生”对象时必须传入姓名和学号)

       2.可以在构造函数中编写自定义逻辑,比如初始化数据、建立连接、设置默认值等

      

  • 未显示定义:在面向对象编程中,构造函数未显式定义指的是开发者没有在类中手动编写构造函数,此时编程语言会自动提供一个默认的构造函数(也叫“无参构造函数”)

       1.没有参数,因此创建对象时无需传入任何值

       2. 通常不包含自定义逻辑,仅完成对象的基础初始化(比如为成员变量设置默认值,如               数值型为0、引用类型为nul等)

       

       总结:

  • 未显式定义构造函数:编译器自动生成默认无参构造函数,可无参创建对象
  • 显式定义构造函数:必须按定义的参数规则创建对象,默认构造函数需手动定义才会存在

2、析构函数 

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

<1>析构函数的特点:

  • 析构函数名是在类名前加上字符~
  • 无参数无返回值。(这里跟构造类似,也不需要加void)
  • ⼀个类只能有⼀个析构函数。若未显式定义,系统会自动生成默认的析构函数
  • 对象生命周期结束时,系统会自动调用析构函数
  • 跟构造函数类似,我们不写编译器自动生成的析构函数对内置类型成员不做处理,自定类型成员会调用他的析构函数
  • 还需要注意的是我们显示写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数
  • 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显示写析构,如MyQueue;但是有资源申请时,⼀定要自己写析构,否则会造成资源泄漏,如Stack
  • ⼀个局部域的多个对象,C++规定后定义的先析构
  • 优点:对比⼀下用C++和C实现的Stack解决之前括号匹配问题isValid,我们发现有了构造函数和析构函数确实方便了很多,不会再忘记调用Init和Destory函数了,也方便了不少

3、 拷贝构造函数

如果⼀个构造函数的第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是⼀个特殊的构造函数

<1>拷贝构造的特点:

  • 拷贝构造函数是构造函数的⼀个重载
  • 拷贝构造函数的第⼀个参数必须是类类型对象的引用,使用传值方式编译器直接报错,因为语法逻 辑上会引发无穷递归调用。拷贝构造函数也可以多个参数,但是第⼀个参数必须是类类型对象的引用,后面的参数必须有缺省值
  • C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返 回都会调用拷贝构造完成
  • 若未显式定义拷贝构造,编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成 员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的拷贝构造
  • 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显示实现拷贝构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显示实现MyQueue的拷贝构造。这里还有⼀个小技巧,如果⼀个类显示实现了析构并释放资源,那么他就需要显示写拷贝构造,否则就不需要
  • 传值返回会产生⼀个临时对象调用拷贝构造,传值引用返回,返回的是返回对象的别名(引用),没有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于⼀个野引用,类似⼀个野指针⼀样。传引用返回可以减少拷贝,但是⼀定要确保返回对象,在当前函数结束后还在,才能用引用返回

 Attention:

  • C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里传值传参要调用拷贝构造
  • 所以这里的d1传值传参给d要调用拷贝构造完成拷贝,传引用传参可以较少这里的拷比

4、赋值运算符的重载

4.1运算符重载

  • 当运算符被用于类类型的对象时,C++语言允许我们通过运算符重载的形式指定新的含义。C++规 定类类型对象使用运算符时,必须转换成调用对应运算符重载,若没有对应的运算符重载,则会编译报错
  • 运算符重载是具有特殊名字的函数,他的名字是由operator和后面要定义的运算符共同构成。和其他函数⼀样,它也具有其返回类型和参数列表以及函数体
  • 重载运算符函数的参数个数和该运算符作用的运算对象数量⼀样多。⼀元运算符有⼀个参数,二元运算符有两个参数,二元运算符的左侧运算对象传给第⼀个参数,右侧运算对象传给第二个参数
  • 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运算 符重载作为成员函数时,参数比运算对象少⼀个
  • 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致
  • 不能通过连接语法中没有的符号来创建新的操作符:比如operator@
  • .*  ::  sizeof  ?:  . 注意以上5个运算符不能重载(选择题里面常考,大家要记⼀下)
  • 重载操作符至少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如: int operator+(int x, int y)
  • ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义,比如Date类重载operator-就有意义,但是重载operator+就没有意义
  • 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,方便区分
  • 重载>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调用时就变成了对象

 Attention:

  1. “operator +”必须至少有⼀个类类型的形参
  2. C++规定成员函数要加&才能取到函数指针
  3. 对象调用成员函数指针时,使用.*运算符

重载为全局的面临对象访问私有成员变量的问题有几种方法可以解决:

  1. 成员放公有
  2. Date提供getxxx函数
  3. 友元函数
  4. 重载为成员函数

4.2赋值运算符重载 

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

赋值运算符重载的特点:

  • 赋值运算符重载是⼀个运算符重载,规定必须重载为成员函数。赋值运算重载的参数建议写成 const当前类类型引用,否则会传值传参会有拷贝
  • 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持连续赋值场景
  • 没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数
  • 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就 可以完成需要的拷贝,所以不需要我们显示实现赋值运算符重载。像Stack这样的类,虽然也都是 内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部 主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载, 也不需要我们显示实现MyQueue的赋值运算符重载。这里还有⼀个小技巧,如果⼀个类显示实现 了析构并释放资源,那么他就需要显示写赋值运算符重载,否则就不需要

4.3日期类的实现

课后小作业,完成各种运算符的重载

5、取地址运算符的重载

5.1const成员函数

  • 将const修饰的成员函数称之为const成员函数,const修饰成员函数放到成员函数参数列表的后面
  • const实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。 const修饰Date类的Print成员函数,Print隐含的this指针由Date* const this 变为 const Date* const this

5.2取地址运算符重载

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

(6)再探构造函数

  • 之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有⼀种方式,就是初始化列表,初始化列表的使用方式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后面跟⼀个放在括号中的初始值或表达式
  • 每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方
  • 引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错
  • C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的
  • 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这 个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误
  • 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致

初始化列表总结:

  1. ⽆论是否显示写初始化列表,每个构造函数都有初始化列表;
  2. ⽆论是否在初始化列表显示初始化成员变量,每个成员变量都要走初始化列表初始化;
  3. 注意这里不是初始化,这里给的是缺省值,这个缺省值是给初始化列表的;如果初始化列表没有显示初始化,默认就会用这个缺省值初始化

 (7)类型转化

  • C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数
  • 构造函数前面加explicit就不再支持隐式类型转换
  • 类类型的对象之间也可以隐式转换,需要相应的构造函数支持

Attention:

  1. 1构造⼀个A的临时对象,再用这个临时对象拷贝构造aa3;编译器遇到连续构造+拷贝构造->优化为直接构造  

(8)static成员

  • 用static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进行初始化
  • 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区
  • 用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针
  • 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针
  • 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数
  • 突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数
  • 静态成员也是类的成员,受public、protected、private访问限定符的限制
  • 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表

(9)友元函数 

  • 友元提供了⼀种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到⼀个类的里面
  • 外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • ⼀个函数可以是多个类的友元函数
  • 友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员
  • 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元
  • 友元类关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元
  • 有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用

(10)内部类

  • 如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类
  • 内部类默认是外部类的友元类
  • 内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考 虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了

(11)匿名对象

  • 用类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参)定义出来的叫有名对象
  • 匿名对象生命周期只在当前一行,⼀般临时定义⼀个对象当前用⼀下即可,就可以定义匿名对象

(12)对象拷贝时的编译器优化

  • 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少⼀些传参和传返回值的过程中可以省略的拷贝
  • 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新⼀点的编 译器对于连续⼀个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化
  • linux下可以将下面代码拷贝到test.cpp⽂件,编译时用g++ test.cpp -fno-elideconstructors的方式关闭构造相关的优化


网站公告

今日签到

点亮在社区的每一天
去签到