Bo2SS

Bo2SS

1 类和对象

课程内容#

类型和变量#

可理解为类和对象的别名

  • image-20210723211429904
  • C++ 中只规定了类型的最小位数,所以有的编译器可以实现更多位数的

类型#

= 类型数据 + 类型操作

  • 例如: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()返回值进行构造

image-20210723211452133

输出结果:

image-20210723211500226
  • 理论上:应该输出 1 次 transform 和 2 次 copy
    • 详见具体分析👇
  • 实际上:没有输出 copy,且 a.x 的值为对象 temp 中的 x 值,即局部变量 temp 的地址直接使用了对象 a 的地址(先开辟的对象 a 的数据区,见下)
    • temp 更像是一个引用
    • 存在编译器优化,即返回值优化 RVO🌟

具体分析#

对象初始化过程:1、开辟对象数据区➡️2、匹配构造函数➡️3、完成构造

  • 了解对象初始化过程后,再分析上面代码中的构造过程:
    • image-20210723211510540
    • 首先,开辟对象 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

image-20210723211519691
  • 出现了两次额外的「拷贝构造」,具体分析见上

注意点#

  • 编译器本质上是通过替换 this 指针实现 RVO 的
  • 因为编译器一般会对拷贝构造进行优化,所以在工程开发中,不要改变拷贝构造的语义
    • 即:拷贝构造中只做拷贝操作,而不要做其它操作
    • 如:拷贝构造中,对拷贝的属性加 1,就不符合拷贝构造的语义,编译器把拷贝构造优化掉后,结果与优化前不一致

+ 赋值运算的优化#

也存在RVO——优化了1次拷贝构造,即将局部变量拷贝给临时匿名对象

image-20210723211527733
  • 添加了红框部分代码
  • 优化前结果:
    • image-20210723211535203
    • 1 次「转换构造」 + 1 次「拷贝构造」
  • 优化后结果:
    • image-20210723211540883
    • 1 次「拷贝构造」

+ 拷贝构造函数的调用分析#

多种写法下,拷贝构造函数是如何调用的?

场景:类 A 中包含一个自定义类 Data 的对象 d,红框为添加的代码

image-20210723211549347

「主要关注第 29 行;关闭 RVO 编译,否则会跳过拷贝构造函数」

  1. 自定义拷贝构造函数,并且对每个成员属性都进行了显式拷贝,即第 29 行不变
  • image-20210723211557942
  • 在构造对象 d 时,会调用 Data 类的拷贝构造函数
  1. 自定义拷贝构造函数,不对每个成员属性进行显式拷贝,即删去 ",d (a.d)"
  • image-20210723211603337
  • 在构造对象 d 时,会调用 Data 类的默认构造函数(自定义时没显式拷贝,则匹配默认构造)
  1. 不自定义拷贝构造函数,编译器自动为其添加默认的拷贝构造函数,即删去第 29~31 行
  • image-20210723211610555
  • 在构造对象 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 无关

代码演示#

类的示例#

image-20210723211620159
  • 类中属性和方法的声明和定义,建议分开
  • this 指针只在成员方法中使用,它指向当前对象的地址

简单实现 cout#

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

构造函数和析构函数#

image-20210723211634762 image-20210723211642463

运行结果:

image-20210723211648649

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 行)就完成了
  • ⚠️:
    • 编译器会默认添加默认构造函数和拷贝构造函数
    • 构造行为都应该放在编译器所谓的构造里,如使用初始化列表

+)左值引用#

  • 后续学习

+)友元函数#

  • 在类内声明(同时保证是类的管理者批准的)
  • 在类外实现,本质是类外的函数,但可以访问类内的私有成员属性

深拷贝、浅拷贝#

拷贝对象:数组

image-20210723211710169
  • 编译器默认添加的拷贝构造为浅拷贝
    • 对于指针,只拷贝地址,所以对拷贝后的对象的修改,会修改原对象
  • 自定义深拷贝版的拷贝构造函数
    • 对于指针,拷贝其指向地址的值
    • PS:构造函数只有在初始化时才被调用,不存在自己拷贝自己的行为

⚠️:

  • 为了适配重载 "<<" 时的 const 参数,需要实现 const 版的 "[]" 重载
  • Array 类里的 end 和数组结尾数据没关系,他只是用来监控数组越界的情况

new、malloc 差别分析#

image-20210723211717190

运行结果:

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

类属性与方法、const、mutable#

image-20210723211734657
  • 类属性:带 static 在类内声明,不带 static 在类外初始化
  • 类方法:可以通过对象或类名两种方式调用它
    • 因此,对象调用的方法不一定是成员方法,还可能是类方法
  • const:const 对象只能调用 const 方法,const 方法内只能调用 const 方法
  • mutable:其修饰的变量可在 const 方法中改变

default、delete#

功能需求:使某个类下的对象,不能被拷贝

image-20210723211742795
  • 禁用拷贝构造函数和赋值运算符
    • 拷贝构造函数:设置为 = delete,或者,将其放在 private 权限中
    • 赋值运算符重载函数:同样设置为 = delete(永远不能使用赋值运算),或者,将其放在 private 权限中(只有类内方法可以使用赋值运算)
    • PS:赋值运算符要同时考虑 const 版和非 const 版
  • ❓要真正实现对象不能被拷贝,应该将拷贝构造函数和赋值运算符重载都设置为 = delete,否则类内方法还是可以拷贝该对象

附加知识点#

  • 构造函数后的花括号
    • 加花括号,表示是一个函数实现
    • 不加花括号,则只是一个函数声明,还需要写一个专门的实现

思考点#

  • 要关注编译器默认做的很多事情,这是 C++ 复杂的地方

Tips#

  • C++
    • 学习方法:按照编程范式分门别类地学习
    • 学习重点:程序的处理流程 [远比 C 复杂]

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.