Cherno笔记 51-70
课程地址:Youtube上TheCherno上传的视频
翻译:神经元猫的高质量翻译
51 Making and Working with Libraries in C++ (Multiple Projects in Visual Studio)
讲如何在vs中建立多个项目,以及如何创建一个库让所有项目都能用。
- 确保目标project的配置类型为静态类(.lib)。
- 设置“附件包含目录”以便能在include时能正确寻找到目标文件。
- 添加引用,这会帮我们自动设置连接器输入,并且改名时不受影响,还会自动创建完整的依赖关系图,先编译依赖后编译此文件。
由于是静态链接,移动后仍然可用,若为动态链接则必须和dll文件在同一目录下。
52 How to Deal with Multiple Return Values in C++
法1 返回struct
法2 传入存结果的指针,没有动态分配
从技术上来说可能是最理想的方法,因为没有复制。
法3 返回数组——只有类型相同时才适用
声明:static std::string* ParseShader(const std::string& filepath);
或 static std::array<std::string, 2> ParseShader(const std::string& filepath);
改用vector也行
返回:return new std::string[]{vs, fs};
或return std::array<std::string, 2>(vs, fs);
new会导致堆分配的发生,同时只适合返回单一类型。
array会在栈上创建,而vector底层存储是在堆上。
法4 返回tuple或pair,容易搞混
#include
#include
static std::tuple或者pair<返回类型1,返回类型2> func()
return make_pair(返回值1,返回值2)
从tuple 中获得值
source=返回的元组(tuple or like pair)
std::get<0/1/2/3>(tuple名source);
pair 的类型,可以用 source.first
53 Templates in C++
template与“泛型”不同,也不能说完全不同,比泛型牛逼的多(泛型非常受限于类型系统)。template有点像宏。
template允许定义一个可根据用途进行编译的模板(让编译器基于一套规则,为你写代码)。
模板不是实际存在的函数,只有当调用时才会被实际创建。
1 |
|
模板绝不仅限于类型或则任何东西,也不限于函数,可创建一整个类。
1 | template<typename T, int N> |
54 Stack vs Heap Memory in C++
堆内存和栈内存的 内存分配 方式不同。
栈分配非常快,所做的就是移动栈指针,就像一条CPU指令。栈大小比如2M。
堆内存分配是一堆事情,程序维护空闲列表,如果需要内存过大还会向OS申请内存,这非常耗费资源。
使用上,尽量使用堆,除非需要变量生存期大于作用域,或需要很大的内存。
对于 struct Vector3 {float x, y, z;} 堆分配使用Vector3* hvector = new Vector3();
或Vector3* hvector = new Vector3;
都是可以的
55 Macros in C++
宏是一个很宽泛的概念,这里指C++中使用预处理器来“宏”化某些操作。
预处理器处理宏发生在预编译期,所做的就是查找和替换——所以不要加分号
1 |
|
编程习惯:不要向人炫耀你知道所有的C++特性,尤其是高级特性更应当减少使用(新手常犯的误区)。
使用宏会让别人困惑,别人得去找到宏才能看懂。
适合用宏的场景:在现实开发中在开发阶段需要日志系统,在发布阶段可能需要去掉(减少透露信息,提升性能)。
在VS项目属性设置中,在**“DEBUG”配置中预处理器-预处理器定义中设置 PR_DEBUG=1
** (注意不要有空格,不过好像有也没事)
1 |
|
可以使用反斜杠对换行符进行转义,注意后面不要有空格,否则就是对空格转义而非对换行符转义。
1 |
56 The “auto” keyword in C++
两面性:一方面API改变修改返回值类型后客户端不需要修改,另一方面可能会破坏依赖于特定类型的代码、还会降低可读性(别人不能很快明白类型)。
推荐:如果类型非常长就推荐用,此时当进入非常复杂的代码集,包含了模板等,就不得不用auto,因为不知道类型是什么(不要写这么复杂的代码,这很难向别人解释代码如何工作,很难维护)。
auto不会加引用,存储返回结果得用 const auto& res =
57 Static Arrays in C++ (std::array)
std::array
也是放在栈上,和普通数组的工作方式基本一样,不需要手动维护数组大小,还能使用sort等标准函数,还能可选边界检查(Release模式下无,边界检查会影响程序性能)。而std::vector是在堆上。
std::array 的size直接返回模板参数,不是返回size变量等——不额外占用空间。
std::array 的实现相当高效,应该用其替代C风格的数组。
头文件如果是一个模板类,头文件就是所需的全部,不需要实现文件。
58 Function Pointers in C++
本节课讲原始C风格的函数指针。
函数指针是将一个函数赋值给一个变量的方法,这与我们之前通过()调用函数的使用方法不同,扩展:还能将函数作为参数传给其他函数。
1 | void HelloWorld(int a) { |
lambda 本质上就是一个普通函数,只是它不像普通函数这样声明,它是在我们的代码在过程中生成的,用完即弃的函数。
[ ] 叫做 捕获方式,即如何传入传出参数。
注意下面代码中 函数指针 的写法。
1 |
|
59 Lambdas in C++
lambda本质上是定义一种叫作匿名函数的方式,不需要实际创建一个函数,就像一个快速的一次性函数,展示下需要运行的代码。它更像一个变量,而非一个正式的函数,在实际编译的代码中作为一个符号存在。
用法:在我们会设置函数指针指向函数的任何地方,都可以将它设置成lambda。我们所做的就是构造一个稍后会调用的函数。
[ ] 捕获方式:打算如何传递变量。
- [=] 全部按值传递。
- [&] 全部按引用传递。
- [a, &b] a通过值传递,b通过引用传递。
非捕获lambda可以隐式转换为函数指针,而有捕获lambda不可以。
1 |
|
对于有捕获的lambda,如果使用
void ForEach(const std::vector<int>& values, void(*func)(int)) {
:
1
2
3
4
5
6
7
8
9
10
11
12 a.cpp: In function 'int main()':
a.cpp:20:19: error: cannot convert 'main()::<lambda(int)>' to 'void (*)(int)'
20 | ForEach(values, lambda);
| ^~~~~~
| |
| main()::<lambda(int)>
a.cpp:7:52: note: initializing argument 2 of 'void ForEach(const std::vector<int>&, void (*)(int))'
7 | void ForEach(const std::vector<int>& values, void(*func)(int)) {
| ~~~~~~^~~~~~~~~~
a.cpp:18:7: warning: unused variable 'a' [-Wunused-variable]
18 | int a = 5;
| ^
60 Why I don’t “using namespace std”
使用using namespace std后就不知道所知用的函数来自哪个namespace。公司可能会有自己的stl版本,比如EASTL,std::vector和eastl::vector好区分,而使用using namespace 语句后就没那么容易区分了。
绝对不要在头文件中使用using namespace。
cherno也会用using namespace,但只会using自己的库,而不会使用std或者eastl。
如果确实要使用using namespace,就在一个足够小的作用域下使用。
61 Namespaces in C++
C中无命名空间,不能将一个函数命名为Init,得叫glfwInit,glBegin等。
C++中有命名空间,主要目的是避免命名冲突,希望能够在不同的上下文中调用相同的符号。
::
是命名空间操作符。
类本身也是一种命名空间;
枚举不是命名空间,而枚举类是。
一般在头文件.h中禁止使用using namespace,因为这样会导致别人引用你的头文件时出现冲突。在cpp里你可以用,但为了养成良好习惯一般用什么写什么,比如using std::vector;
1 | using orange::print(); // 只为print指定命名空间,print_again需再指定 |
严肃项目中,所有函数都应放在namespace xxx {} 里面。
62 Threads in C++
如果不使用线程,无法让程序同时做两件事(比如同时等待用户输入,和向控制台打印消息)。
1 |
|
63 Timing in C++
标准写法可参考 74 。
chrono是C++库的一部分(C++11),不需要使用操作系统。在chrono之前,如果想要非常精确的计时器,需要使用操作系统库(Windows中,有QueryPerformanceCounter)。
chrono与平台无关,推荐使用(99%情况下使用chrono就好),除非正在做一些特定的底层的事情希望进一步减少开销,或者想要使用特定于平台的库。
1 |
|
instrumentation(插码)
,可以使用插码来实际修改源代码,以包含某种分析工具。
std::endl
很慢,推荐换成 \n。
记时类
1 |
|
64 Multidimensional Arrays in C++ (2D arrays)
在处理任何类型的数组时,指针都是重要的,因为数组就是内存块,处理内存简单的方式就是使用指针。
二维数组就是数组的数组,是数组的集合,int**
就是指向int*
数组的指针。
int** a2d = new int*[5]
所做的就是分配20byte的空间(32位),这个空间后续可以存放其他类型数据比如float。
1 | int main() { |
堆分配方式,会被分配到内存中完全随机的位置,可能很远——要访问全部数据,可能在访问一行数据后,要跳到其他维度,这会导致 cache miss(缓存不命中)
,这意味着我们在从ram中获取数据时浪费了时间。
内存分配越分散性能越差。优化时尽量让访问的内存存储在一块,这样定位数据时会有更多的cache hits以及更少的cache miss。
65 Sorting in C++
1 | std::vector<int> values = { 3, 5, 1, 4, 2 }; |
66 Type Punning in C++
类型双关只是一个花哨的术语,用来在C++中绕过类型系统。
C++是强类型语言,有类型系统。C++中虽然类型是由编译器强制执行的,但可以直接访问内存,这是一种原始的、底层的访问。
1 | int a = 50; |
类型双关:double value = *(double*)&a;
取a的地址转换为double*
指针,然后再解引用。这样处理会把a后面4字节的内存也复制过来。
PS:使用引用则为double& value = *(double*)&a;
理解:把目标内存当作不同类型的内存来对待。我们所需要做的只是将该类型作为指针,然后将其转换为另一个指针,如果由必要还可以进行解引用。
结构体(类)本身不包含任何数据的填充。
1 |
|
67 Unions in C++
联合体有点像类类型或结构体类型,只不过它一次只能占用一个成员的内存。类中各个成员共享内存,将a的值修改为5则d的值也会修改为5。
可以像使用结构体和类一样使用联合体,可以给它添加静态函数或普通函数、方法等。然而,不能使用虚函数,还有其他一些限制。
通常人们用联合体来做的事是和类型双关紧密相关的。
当想给同一个变量取两个不同的名字时,或想用多种方法来处理相同的数据时,联合体很有用。
通常union是匿名使用的,但是匿名union不能含有成员函数。
1 | struct Vector2 { |
68 Virtual Destructors in C++
虚析构函数就是虚函数和析构函数的组合。虚析构函数对于处理多态非常重要。
在普通方法前标记为 virtual,那么它就可以被覆写,这意味着虚函数表要做这样的设置。
虚析构函数有点不同,虚析构函数不是要覆写析构函数,而是加上一个析构函数。
💡 只要允许一个类拥有子类,百分百需要声明析构函数是虚函数,否则没人能安全地扩展此类。
1 |
|
注意:正常情况下,创建子类对象时,会先调用父类构造函数,再调用子类构造函数。
删除子类对象时,会先调用子类析构函数,再调用父类析构函数。
69 Casting in C++
本节需要练习并通过经验来学习,而不是告诉它如何运作。这个话题上若只使用理论而没有实践,则不会有多大帮助。
(int)value
是 C语言风格类型转换。圆括号中指定了要强制转换的类型,然后是我们要强制转换的变量,变量可用括号括起来。
C++风格类型转换共有4中主要的cast,它们不能做任何C风格类型转换所不能处理的事情。这不是添加新功能,只是添加一些语法糖(dynamic_cast会做运行时检查,其他的会做编译时检查)。
- static_cast 用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型之间的互相转换**,不能用于指针类型的强制转换。**
- reinterpret_cast 用于进行各种不同类型的指针之间强制转换,把目标内存重新解释成别的东西
double s = reinterpret_cast<AnotherClass*>(&value) + 5.3;
。 - const_cast 用于进行去除 const 属性的转换。
- dynamic_cast 不检查转换安全性,仅运行时检查(与运行时状态信息RTTI紧密相关),如果不能转换,返回null。
搞这么多cast的好处:除了可能收到检查外,还可以在代码库中搜索它们。
70 Conditional and Action Breakpoints in C++
普通断点的缺点:需要停止应用程序,然后重新编译;有时还很难复现bug状态。
条件断点:可以告诉调试器,我想在此放置一个断点,但我只希望断点在特定条件下触发。
操作断点:允许我们采取某种动作,一般是在碰到断点时打印一些东西到控制台。
右键断点,选择选择”条件“或者”操作“,”操作”中还能取消勾选“继续执行”。