在观看了 Cherno 的 C++ 系列视频后的一些记录——基础篇
2024-07-19 19:58:53
目录:
头文件
C++ 采用了使用头文件来包含声明的约定。在一个头文件中进行声明,然后在每个.cpp
文件或其他需要该声明的头文件中使用#include
指令,#include
指令在编译之前将头文件的副本插入.cpp
文件。
引号可以引用所有文件,但是一般用于源文件所在目录的头文件,而尖括号仅用于标准库标头。是否有.h
扩展是一种区分 C 标准库和 C++ 标准库的方法。
通常,头文件有一个 include 防范或 #pragma once
指令,用于确保它们不会多次插入到单个 .cpp 文件中。
循环语句(for、while)
语法:
1 | for (init-statement condition; expression) statement |
for 语句等价于:
1 | { |
以下是两者的异同:
- init-statement 和 condition 的作用域相同
- statement 和 expression 的作用域不相交,但都内嵌在 init-statement 和 condition 的作用域中
类和结构体中的静态(static)
当数据成员被声明为static
时,只会为类的所有对象保留一份数据副本。静态数据成员不是给定的类类型的对象的一部分。因此,静态数据成员的声明不被视为一个定义。在类范围内声明数据成员,但在文件范围内执行定义。这些静态类成员具有外部链接。
静态数据成员遵循类成员访问规则,因此只允许类成员函数和友元拥有对静态数据成员的私有访问权限。静态方法不能访问非静态变量,因为静态方法没有类实例。本质上在类中写的每一个方法总是获得当前类的一个实例作为参数,在类中你看不到这种东西,它们通过隐藏参数发挥作用,而静态方法不会得到这个隐藏参数。静态方法与在类外部编写方法相同
局部静态(static)
局部静态牵扯到了单例类,不了解单例类,暂时搁置
枚举
枚举只是一种命名值的方法,如果你有一个数值集合,而你想用数字来表示它们,枚举就是你想要的
构造、析构函数
默认构造函数通常没有参数,但它们可以具有带默认的参数。
1 | class Entity{ |
默认构造函数是特俗成员函数之一。如果类中未声明构造函数,则编译器提供隐式inline
默认构造函数。如果你依赖于隐式默认构造函数,请确保在类定义中初始化成员。如果没有这些初始化表达式,成员会处于未初始化状态,Volume() 调用会生成垃圾值。一般而言,即使不依赖于默认构造函数,也最好以上述方式初始化成员。
当你不希望有人创建类实例时,有两种不同的解决方法:
可以通过设置为
private
来隐藏构造函数1
2private:
Log() {}C++ 为我们提供了一个默认构造函数,我们可以告诉编译器,我不想要那个默认构造函数
1
Log() = delete;
析构函数同时适用于堆和栈分配的对象,如果你使用new
分配一个对象,通过调用delete
或delete []
显示销毁对象时,析构函数就会被调用。而如果只是一个栈对象,当作用域结束对象超出范围时,栈对象将被删除,析构函数也会被调用
虚函数、纯虚函数(接口)
虚函数引入了一种叫做 Dynamic Dispatch(动态联编)的东西,它通过虚函数表来实现编译,虚函数表就是一个表,它包含基类中所有虚函数的映射,这样我们可以在它运行时,将它们映射到正确的override
函数
1 | class Entity { |
虚函数并不是免费的(无额外开销的),有两种与虚函数相关的运行时成本:
- 我们需要额外的内存来存储虚函数表,这样就可以分配到正确的函数,包括基类中要有一个成员指针,指向虚函数表
- 每次我们调用虚函数表时,我们需要遍历这个表,来确定要映射到哪个函数
可能在一些嵌入式平台上 cpu 性能很差,避免使用虚函数,除此之外,它的影响小到你可能都不会注意到
纯虚函数本质上与其他语言(如Java或C#)中的抽象方法或接口相同,它允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数,只有实现了纯虚函数之后,才能够实例化
1 | class Entity { |
数组
对于for
循环,一般不采用 <= 或 >= ,这涉及到性能问题,因为你在做小于以及等于的比较,所以它必须做等于比较,而不仅是小于比较
在堆上创建数组,动态地用new
来分配,最大的原因是生存期,用new
分配的内存,它将一直存在,直到你删除它。在栈上创建数组可以避免间接寻址,因为在内存中跳跃肯定会影响性能。
实际上没有办法计算出原始数组的大小,需要自己维护自己的数组大小
1 | static const int size = 5; |
在 C++11 中引入了标准数组std:::array
,它有边界检查,可以记录数组大小
1 | std::array<int, 5> another; |
字符串
std::string
的本质是一个const char*
数组,很多std::string
可行而const char*
不可行的操作都是基于对操作符的重载后实现的
举个例子,不引入头文件string
时<<
会报错,这是因为<<
允许我们发送字符串到流中的重载版本是在string
头文件内部
const 关键字
const
是一种承诺,它承诺了一些东西是不变的,但并不意味着不可以违背,在这里提出来只是想告诉你在计算机中没有什么是真正一成不变的,话说回来这是一个承诺,你应该遵守承诺
最基本的用法:
1 | const int kMaxAge = 90; |
还有几种用法,首先是指针:
1 | const int *ptr = new int; // int int const *ptr = new int; |
最后是在类中以及方法中使用const
:
1 | class Entitty { |
有一种const
的情况,你确实想要标记方法为const
,但由于某些原因,又确实需要修改一些变量:
1 | class Entitty { |
mutable 关键字
在类成员中使用mutable
可能是唯一你会使用到它的情况了,例如为了调试,我们想计算一下函数在程序中被调用了多少次:
1 | class Entitty { |
但是,还有一个用mutable
的地方,就是lambda
:
1 | int x = 8; |
成员初始化列表
成员初始化列表不仅使代码非常干净,易于阅读,这实际上也有一个功能上的区别,在特定的类的情况下,会提升性能:
1 | class Entity { |
尽管对于整数这样的基本类型,它不会被初始化,除非你通过赋值来初始化它们,但是没有必要区分基本类型和类类型,你应该在所有地方都使用初始化列表
创建并初始化 C++ 对象
在栈上创建对象:
1 | Entity e; |
在堆上创建对象:
1 | Entity e; |
这就是我们创建对象的两种方法,如何选择呢,如果对象太大或者你要显示的控制对象的生存期,那就在堆上创建。如果你忽略这两个选择的话,那就在栈上分配,栈上分配自动化而且更快,而在堆上分配,需要你手动delete
,对此你可以使用智能指针,这点之后再谈
【39】C++ new 关键字
new
返回指向你分配的内存的空指针(空指针基本上是一个没有类型的指针,指针只是一个内存地址,指针之所以需要类型,是因为你需要类型来操纵它)
new
不仅分配内存,它还调用构造函数,这背后的真相是它是一个操作符,这意味着你可以重载这个操作符,并改变它的行为。通常调用new
,会调用隐藏在里面的 C 函数
关于new
想说的最后一点是当使用了new
关键字时,得记住必须要用delete
1 | Entity* e = new Entity(); |
【40】C++ 隐式转换与explicit
关键字
C++ 实际上允许编译器对代码执行一次隐式转换。当explicit
放在构造函数前面,这意味着没有隐式转换,如果要构造对象则必须显示调用此构造函数
【42】C++ 的this
关键字
在 C++ 中为了调用一个非静态方法,需要首先要实例化一个对象,然后调用这个方法。而this
是一个指向当前对象实例的指针,这实际上对方法的一般工作方式很重要,在类中你可以通过this
创建对象来调用某个方法
排序
1 | sort(nums.begin(), nums.end(), [](int a, int b) { |
联合体
联合体一次只能占据一个成员的内存,假如 union 内定义了float x, y, z, w;
,这里换成类或者结构体所占用的内存应该是 16 个字节,但是联合体所占内存为 4 个 字节,并且如果初始化 w 为 5.0f,那么其他三个成员数据的值均为 5.0f
现在是不是有点眼熟啊,没错,这里联合体就是起到了类似类型双关的作用,将一块内存用不同的变量(或者类型)来解释。有一点需要提及的是,联合体通常是匿名使用,匿名联合体不能包含成员函数