课程内容#
类型和变量#
可理解为类和对象的别名
-
- C++ 中只规定了类型的最小位数,所以有的编译器可以实现更多位数的
-
- 参考 cppreference——基础类型
-
类型#
= 类型数据 + 类型操作
- 例如:int=4 字节大小的数据 + 基本操作(
+-*/%
),可联想数据结构,int、double 类型本质都是数据结构 - 类型数据 + 类型操作 ➡️成员属性 + 成员方法
- 可理解为加强版的 C 语言 struct(可以放属性,但不能放方法)
访问权限
先区分类内和类外的概念
访问权限在类内设置,它控制的是类外能不能访问类内
- public:公共
- 类内和类外都可以访问它
- private:私有
- 只有类内的方法可以访问它
- protected:受保护
- 除了类内,继承的类内也可以访问它
- friendly:友元
- 它修饰的函数可以访问类内的私有成员和受保护成员
构造函数和析构函数#
对象的生命周期:构造→使用→析构
三种基本的构造函数#
联想局部变量的初始化,对象也需要初始化
构造函数类型 | 使用方式 | ⚠️注意 |
---|---|---|
默认构造函数 | People bob; 原型:People (); | 1、零参构造函数 2、编译器自动生成的 |
转换构造函数 | People bob("DoubleLLL"); 原型:People (string name); | 1、一个参数的有参构造函数 2、该参数传递给类的成员变量,且不是本类的 const 引用 [PS] 有点像隐式的类型转换 |
拷贝构造函数 | People bob(hug); 原型:People (const People &a); | 1、特殊的有参构造函数,传入的是本类的对象 2、与赋值运算符 "=" 不等价 [PS] 处理为 const & 更方便 |
析构函数#
销毁对象
原型:~People ();
⚠️注意:
1、没有参数,也没有返回值
2、资源回收时使用
小结#
都没有返回值,函数名与类名一致
- 在工程开发中,构造函数和析构函数的功能会设计得非常简单
- ❓为什么不在构造函数中进行大量的资源申请?
- 原因:构造函数的 Bug,编译器较难察觉
- 解决方式:伪构造函数、伪析构函数;工厂设计模式
- [+] 移动构造函数(另一个关键构造函数,后续学习)
- 来自 C++ 11 标准 ——C++ 重新归回神坛的标准
- 在此之前,STL 的性能低下,因为 C++ 的语言特性不好,它没有区分左值、右值概念,使得 STL 使用过程中发生的大量拷贝操作,尤其是深拷贝操作,会大大影响性能
- 在此之后,有了右值的概念,引入了移动构造函数
返回值优化(RVO)#
编译器默认开启了 RVO
引入#
对象a通过fun()返回值进行构造

输出结果:

- 理论上:应该输出 1 次 transform 和 2 次 copy
- 详见具体分析👇
- 实际上:没有输出 copy,且 a.x 的值为对象 temp 中的 x 值,即局部变量 temp 的地址直接使用了对象 a 的地址(先开辟的对象 a 的数据区,见下)
- temp 更像是一个引用
- 存在编译器优化,即返回值优化 RVO🌟
具体分析#
对象初始化过程:1、开辟对象数据区➡️2、匹配构造函数➡️3、完成构造
- 了解对象初始化过程后,再分析上面代码中的构造过程:
-
- 首先,开辟对象 a 的数据区
- 然后,进入 func 函数,开辟对象 temp 的数据区,通过「转换构造」初始化局部变量 temp——A temp (69);
- 再者,通过「拷贝构造」将 temp 拷贝给临时匿名对象,并销毁对象 temp——return temp;
- 最后,通过「拷贝构造」将临时匿名对象拷贝给 a,销毁临时匿名对象 ——Aa= func ();
- 👉由此可见,过程包含 1 次转换构造、2 次拷贝构造
-
- 编译器优化
- 第 1 次优化,取消第 1 次「拷贝构造」,直接将 temp 拷贝给 a(Windows 下的优化)
- 第 2 次优化,取消第 2 次「拷贝构造」,直接是 temp 指向 a(Mac、Linux 下的优化)
关闭 RVO 后#
通过g++ -fno-elide-constructors编译源文件,即可关闭RVO

- 出现了两次额外的「拷贝构造」,具体分析见上
注意点#
- 编译器本质上是通过替换 this 指针实现 RVO 的
- 因为编译器一般会对拷贝构造进行优化,所以在工程开发中,不要改变拷贝构造的语义
- 即:拷贝构造中只做拷贝操作,而不要做其它操作
- 如:拷贝构造中,对拷贝的属性加 1,就不符合拷贝构造的语义,编译器把拷贝构造优化掉后,结果与优化前不一致
+ 赋值运算的优化#
也存在RVO——优化了1次拷贝构造,即将局部变量拷贝给临时匿名对象

- 添加了红框部分代码
- 优化前结果:
-
- 1 次「转换构造」 + 1 次「拷贝构造」
-
- 优化后结果:
-
- 1 次「拷贝构造」
-
+ 拷贝构造函数的调用分析#
多种写法下,拷贝构造函数是如何调用的?
场景:类 A 中包含一个自定义类 Data 的对象 d,红框为添加的代码

「主要关注第 29 行;关闭 RVO 编译,否则会跳过拷贝构造函数」
- 自定义拷贝构造函数,并且对每个成员属性都进行了显式拷贝,即第 29 行不变
-
- 在构造对象 d 时,会调用 Data 类的拷贝构造函数
- 自定义拷贝构造函数,不对每个成员属性进行显式拷贝,即删去 ",d (a.d)"
-
- 在构造对象 d 时,会调用 Data 类的默认构造函数(自定义时没显式拷贝,则匹配默认构造)
- 不自定义拷贝构造函数,编译器自动为其添加默认的拷贝构造函数,即删去第 29~31 行
-
- 在构造对象 d 时,会调用 Data 类的拷贝构造函数(编译器默认的)
结论:
❗️要想达到预期的结果,在自定义拷贝构造函数时,应该对每个成员属性进行显式拷贝
- 如果自定义的拷贝构造函数什么都不写,那该函数就什么都不会做
其它知识点#
引用#
引用就是其绑定对象的别名
- ❗️引用在定义的时候就需要初始化,即绑定对象,如:
- People a;
- 定义时就初始化:People &b = a;
- 否则:People &b; b = c; 会发生歧义 —— 绑定对象还是赋值?
类属性与方法#
加有 static 关键字
区别于成员属性(每一个对象特有的)、成员方法(this 指针指向当前对象)
- 类属性:该类所有对象统一的属性
- 全局唯一、共享
- 例如:全人类的数量 —— 人类中所有对象的数量
- 类方法:不单独属于某个对象的方法
- 不和对象绑定,无法访问 this 指针
- 例如:测试某个高度是否为合法身高
const 方法#
不改变对象的成员属性,不可调用非 const 成员函数
- 提供给 const 对象使用(其任何属性都不能被改变)
⚠️:
mutable:可变的,其修饰的变量在 const 方法中可变
default 和 delete#
默认函数的控制,C++ 11
- default:显式地使用编译器默认提供的规则
- 仅适用于类的特殊成员函数(默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符),且该特殊成员函数没有默认参数
- ❗️该特性没有功能上的意义,这是 C++ 的设计哲学:关注可读性、可维护性等
- 理解这种理念,可以提高自己在 C++ 上的审美标准
- delete:显式地禁用某个函数
struct 和 class#
- struct
- 默认访问权限:public(黑名单机制,需要显式定义 private 成员)
- 也是用来定义类的,有成员属性和成员方法,要与 C 中区分开来
- class
- 默认访问权限:private(白名单机制:需要显式定义 public 成员)
❓:C++ 为什么要保留 struct 关键字?默认权限为什么是 public?
- 都是为了兼容 C 语言,可以减小推广难度
PS:前端语言 JavaScript 为了减小推广难度,从名称蹭 Java 的热度,本质与 Java 无关
代码演示#
类的示例#

- 类中属性和方法的声明和定义,建议分开
- this 指针只在成员方法中使用,它指向当前对象的地址
简单实现 cout#

- cout 是一个对象,是一个高级变量
- 返回其自身引用可以实现连续 cout,使用引用的原因后续理解
- 字符串需要使用 const 类型变量接收,否则会警告,因为是字符串是字面量
- 命名空间的精髓:相同的对象名可以存在不同的命名空间中
构造函数和析构函数#


运行结果:

1)析构顺序的探讨#
-
- 构造顺序:对象 a,对象 b
- 析构顺序:对象 b,对象 a
- ❓为什么析构函数的调用顺序是反过来的?是因为编译器产生的特例,还是一个正常的语言特性?👉语言特性
- 对象 b 的构造可能依赖对象 a 的信息➡️在析构的时候对象 b 也可能会用到对象 a 的信息➡️对象 b 要先于对象 a 析构
- ❗️谁先构造,它就后析构
- PS
- 这与对象放在堆空间还是栈空间无关,实验表明,析构顺序都是反过来的
- 可以认为是一种拓扑序
2)转换构造函数#
- ❓为什么叫做转换构造函数(单一参数的构造函数)
- 将一个变量转化成了该类型的对象
- 🌟a=123 涉及运算符重载:隐式类型转换➡️赋值➡️析构,详解见代码
3)拷贝构造函数#
1、加引用:防止无限调用拷贝构造
- ❗️如果拷贝构造函数为:A (A a) {},则 A b = a; 时,
- 因为 [形参 a] 不是引用(值传递),所以需要先将 [对象 a] 拷贝到 [形参 a] 中生成临时对象
- 此时,又会发生拷贝构造,而这个拷贝构造同样会经历上述过程
- 从而无限递归
- PS:引用不产生任何拷贝行为,比指针更方便
2、加 const:防止 const 类型对象使用非 const 类型的拷贝构造,报错
- 也防止被拷贝的对象被修改
注:
- 在定义对象时,"=" 调用的是拷贝构造函数,而不是赋值运算,例如,A b = a
4)思考#
- 对象是什么时候完成了构造?
- 「参考代码,以默认构造函数为例」
- 功能上的构造 [逻辑上]:到第 46 行,构造函数表面上执行完成了
- 编译器的构造[实际上]:到第 39 行,就已经可以调用对象成员了❗️🌟
- 通过思考部分的代码可以理解:
- 场景:在类 Data 中添加有参构造函数,使编译器帮其添加的默认构造函数被删除
- 过程:在生成类 A 对象时,成员属性 Data 类对象 p、q 需要已经完成构造,而此时 Data 类的默认构造已经被删除了
- 结果
- 如果不使用初始化列表初始化 p、q,则报错
- 如果使用初始化列表,则可行,初始化列表属于编译器所谓的构造
- ❗️这说明编译器的构造是在函数声明后(第 39 行)就完成了
- ⚠️:
- 编译器会默认添加默认构造函数和拷贝构造函数
- 构造行为都应该放在编译器所谓的构造里,如使用初始化列表
+)左值引用#
- 后续学习
+)友元函数#
- 在类内声明(同时保证是类的管理者批准的)
- 在类外实现,本质是类外的函数,但可以访问类内的私有成员属性
深拷贝、浅拷贝#
拷贝对象:数组

- 编译器默认添加的拷贝构造为浅拷贝
- 对于指针,只拷贝地址,所以对拷贝后的对象的修改,会修改原对象
- 自定义深拷贝版的拷贝构造函数
- 对于指针,拷贝其指向地址的值
- PS:构造函数只有在初始化时才被调用,不存在自己拷贝自己的行为
⚠️:
- 为了适配重载 "<<" 时的 const 参数,需要实现 const 版的 "[]" 重载
- Array 类里的 end 和数组结尾数据没关系,他只是用来监控数组越界的情况
new、malloc 差别分析#

运行结果:

- malloc 和 new 都可以申请空间,分别对应 free 和 delete(如果是数组,则为 delete [])销毁空间
- new 会自动调用构造函数,对应的 delete 会自动调用析构函数;而 malloc 和 free 都不会
- malloc +new 可以实现原地构造,常用于深拷贝,其中,new 还可以对应不同类的构造函数
类属性与方法、const、mutable#

- 类属性:带 static 在类内声明,不带 static 在类外初始化
- 类方法:可以通过对象或类名两种方式调用它
- 因此,对象调用的方法不一定是成员方法,还可能是类方法
- const:const 对象只能调用 const 方法,const 方法内只能调用 const 方法
- mutable:其修饰的变量可在 const 方法中改变
default、delete#
功能需求:使某个类下的对象,不能被拷贝

- 禁用拷贝构造函数和赋值运算符
- 拷贝构造函数:设置为 = delete,或者,将其放在 private 权限中
- 赋值运算符重载函数:同样设置为 = delete(永远不能使用赋值运算),或者,将其放在 private 权限中(只有类内方法可以使用赋值运算)
- PS:赋值运算符要同时考虑 const 版和非 const 版
- ❓要真正实现对象不能被拷贝,应该将拷贝构造函数和赋值运算符重载都设置为 = delete,否则类内方法还是可以拷贝该对象
附加知识点#
- 构造函数后的花括号
- 加花括号,表示是一个函数实现
- 不加花括号,则只是一个函数声明,还需要写一个专门的实现
思考点#
- 要关注编译器默认做的很多事情,这是 C++ 复杂的地方
Tips#
- C++
- 学习方法:按照编程范式分门别类地学习
- 学习重点:程序的处理流程 [远比 C 复杂]