Cherno笔记 51-70
Cherno笔记(下)
71 Safety in modern C++ and how to teach it
安全编程:希望降低崩溃、内存泄漏、非法访问等问题。
当我们开始倾向于智能指针之类的东西时,这一切可以归纳为想分配堆内存。
智能指针和自动内存管理系统的存在使得程序员的生活更容易,更有力(当忘记处理某些事情时,它会自动处理)。
省流助手:生产环境使用智能指针,学习使用原始指针,当然,如果你需要定制的话,也可以使用自己写的智能指针(特定平台需要,或者根据平台特点压榨特定平台性能)。
72 Precompiled Headers in C++
预编译的头文件实际上时让你抓取一堆头文件,并将它们转化成编译器可以使用的格式,而不必一遍遍地读取这些头文件。
问题:每次在C++文件中#include<vector>
时,需要读取整个vector头文件并编译, 不仅如此,vector还包含一堆其他的包含文件,这些文件一样需要读取。
每次对C++文件进行修改,整个文件需要重新编译,那么vector头文件又需要复制粘贴、解析、编译(不断解析编译同样的代码)。
预编译头文件的作用:接收一堆你告诉它要接收的头文件,基本上是只能编译一次的一堆代码,它以二进制格式存储。
不该用:不要将频繁更改的文件放入预编译头文件中,否则反而会拖慢运行时间(更改后整个预编译头文件每次需要从头开始重新构建)。此外,如果是像GLFW这样只被include一次的文件,预编译会降低可读性。
预编译头文件应当用于 外部的头文件,比如windows.h和STL,也没理由不用(每个cpp文件都编译一次它们,实在太可怕)。
依赖问题:pch实际做的是把所有东西都塞进pch中,它可能会隐藏实际正在使用的东西。所有依赖都在pch中,单独看CPP文件时,并不知道需要什么依赖。这在模块化和代码重用方面可能非常困难。
VS中操作:
- 头文件属性,在预编译头文件中选择 Create
- 项属性,在预编译头文件中选择 Use,输入文件名 xxx.h
- 查看编译时间:tools - options - projects and solutions - vc++project setting下选显示build time
g++中操作:
g++ -std=c++11 pch.h
time g++ -std=c++1 Main.cpp
73 Dynamic Casting in C++
casting就是强制类型转换。
类型系统在C++中并不是特别强制,它是一种保护代码的方法,不是必须要使用的东西,因为我们可以在类型之前自由转换。
dynamic_cast
是运行时检查,它有相关的运行时成本。
dynamic_cast是专门用于沿继承层次结构进行的强制类型转化,可以从派生类转化为基类,或从基类转化为派生类型。
从子类到父类是隐式转换就能做的(子类一定是父类),用dynamic_cast也可以;
从父类到子类:父类不一定是子类,可以通过dynamic_cast进行验证——如果不是子类,转换失败返回null(0)。
dynamic_cast需要RTTI,RTTI使能区分player和enemy——时空开销
可以关闭RTTI:VS项目属性中,C/C++的Language下有enable RTTI的选项
关闭RTII后不能进行dynamic_cast
1 |
|
demo有内存泄露的问题:没有调用
delete
,懒得加了。
74 BENCHMARKING in C++ (how to measure performance)
基准测试没有标准答案,有许多不同方式,每个人都有自己衡量性能的方式。
这里的基准测试不仅仅是用来对代码进行基准测试的工具,如果想衡量一段C++代码的性能,这段代码本身就需要”正确”地写。
1 |
|
做benckmark时无论在测试什么,都要确保你确实做了这些事情。编译优化可能让这类的计时器结构完全无用,下面代码展示的是Release模式下常量绑定优化的汇编代码结果。
1 | std::cout << value << std::endl; |
一定要确保所分析的代码在发布时是有意义的,要处于Release模式而非Debug模式。
75 STRUCTURED BINDINGS in C++
在 C++ 中,
std::tuple
是一个模板类,它提供了一种方式来存储不同类型的元素集合。std::tuple
是 C++11 标准引入的,它允许你创建一个包含多个元素的单一对象,这些元素可以是不同的类型。
结构化绑定是一个新特性,帮助更好的处理多返回值。
返回结构体其实也不太好:为什么要用一种只使用一次的类型呢,这会使代码库混乱。
1 |
|
结构化绑定需要C++17。
76 How to Deal with OPTIONAL Data in C++
在 C++ 中,
std::optional
是 C++17 标准引入的一个模板类,它提供了一种包装可能存在或不存在的值的方式。std::optional
可以被视为一个容器,它要么包含一个值,要么不包含任何值(即为空)。这使得在函数无法返回有效值时,可以返回一个空的std::optional
对象,而不是使用可能的nullptr
或特殊的返回值。
1 |
|
不是返回空字符串,而是返回optional,{}
不用 if (data.has_value())
用 if (data) 就行,因为data对象有一个bool运算符,这更加干净漂亮。
data.value_or()
在解析文件时非常有用,比如提取变量或任何已设置的元素。
77 Multiple TYPES of Data in a SINGLE VARIABLE in C++?
std::variant
是C++17新出现的,并不是一个真正的特性,更多的是一个在C++17中新的标准库给我们的类。
std::variant作用:让我们不用担心处理的确切数据类型。
我们所做的就是指定一个叫做std::variant的东西,然后列出它可能的数据类型,可以把它重新赋值任何类型。
场景:解析一个文件,不确定是字符串还是整数;或程序运行时接受一个命令行参数,不确定那个参数是整数、字符串、浮点数或布尔值。
data=false
允许我们这样做,但实际上这样以后就不能访问它了。
1 |
|
variant与union不同,variant只是将所有可能的数据类型作为单独的成员变量。
从技术上讲,union仍然是更有效、更好,而variant更加类型安全,不会造成未定义行为。
get_if<类型>()
失败时返回null,需要插入的参数为指针类型
78 How to store ANY data in C++
std::any
可以存储任何类型,和 std::variant
类似。前者不需要列出类型,而后者需要列出类型,绝大部分场景下std::variant会使得类型安全。
variant 和 any 之间的区别,除了variant需要列出类型外,还和它们的存储方式有关。
std::any到底做了什么:对于small types 它只是把它们存储为一个Union,这意味这对于小类型来说它的工作方式和variant完全相同。如果有一个大的类型,any会带入大存储空间void*,在这种情况下,它会动态分配内存——不利于性能。
如果在small type上使用variant或any,比如int,float,一个用于类的vector或类似于math库等,它们会以完全相同的方式工作。但如果需要更多的存储空间,std::any会动态分配,而std::variant不会。
总结:variant除了更加类型安全和有一点限制之外,variant在处理较大数据时也会执行的更快(不需要动态内存分配)。
Cherno认为any有点无用、搞笑,认为自己压根用不到。
1 |
|
79 How to make C++ run FASTER (with std::async)
std::asnyc
(C++11)需要引入future
库。并行计算最难的是找出彼此的依赖关系,并想清楚在不同的线程中放什么。
设置启动类型为async,否则如果设置为deferred等,那么它可能不会在一个单独的线程上完成,C++会根据当前的工作负载来选择。
1 | std::vector<std::future<void>> m_Futures; // 保存返回值,防止局部变量析构 |
1、为什么不能传引用?
线程函数的参数按值移动或复制。如果引用参数需要传递给线程函数,它必须被包装(例如使用std::ref
或std::cref
)
2、std::async为什么一定要返回值?
如果没有返回值,那么在一次for循环之后,临时对象会被析构(规则要求局部变量被析构),而析构函数中需要等待线程结束,所以就和顺序执行一样,一个个的等下去。
如果将返回值赋值给外部变量,那么生存期就在for循环之外,那么对象不会被析构,也就不需要等待线程结束。
需要保留返回值std::future,如果不保留的话,它将被摧毁,在摧毁时需要确保过程实际上已经完成,这基本意味着它根本不是并行的。
Debug→Windows→parallel stacks 中就能看到一堆不同线程的堆栈跟踪。
80 How to make your STRINGS FASTER in C++!
substr()
会创建一个新的字符串,std::string的内存在堆上。
Cherno:如果观察任何一款游戏,并对其进行分析和检查,其中很大一部分由字符串操作造成。
4次调用new导致堆内存分配——可通过重载new运算符查看alloc次数
1 | void PrintName(const std::string& name) { |
1次:
1 | void PrintName(std::string_view name) { |
std::string_view
是 C++17 标准引入的一个非拥有(non-owning)字符串视图类型。它提供了一种轻量级的、不可修改的字符串表示,允许你查看和操作字符串数据,而不需要复制数据。std::string_view
通常用于函数参数,以便在不复制字符串内容的情况下传递字符串。
0次:
1 | void PrintName(std::string_view name) { |
std::string_view (C++17,在这之前许多人自己实现),本质上只是一个只想现有内存的指针。换句话说,就是一个const char*
,指向其他人拥有的现有字符串,再加上一个size。
字符串literal在.rodata
区,常量字符区,而非栈上。
尽量不使用string使用const char*
81 VISUAL BENCHMARKING in C++ (how to measure performance visually)
Chrome Tracing,它很基础(barebone,准系统),也很简单。
chrome://tracing
工作方式是加载一个包含所有数据的json文件
参考项目地址:https://github.com/GavinSun0921/InstrumentorTimer
82 SINGLETONS in C++
singletons
单例是一个类的单一实例(设计模式)。当我们想拥有应用于某种全局数据集的功能时,且我们只是想重复使用时,单例非常有用,比如随机数生成器和渲染器。
从根本上说,单例的行为就像命名空间,单例类可以像命名空间一样工作,并没有要求单例类像普通类一样工作。
C++中单例只是一种组织一堆全局变量和静态函数的方式。
静态函数有时可能对这些变量起作用,有时可能不起。
1 | class Random { |
- 不能有公共的构造函数,需要标记为private,这意味着类不能在外部被实例化
- 提供一种静态访问该类的方法
- 如果
delete
运算符的操作数是可修改的左值,则在删除该对象后未定义其值。
使用 namespace 也可以,使用类没什么真正的缺点,它确实让你的代码有条理。
83 Small String Optimization in C++
很多人会因为你不用他们期待的高效方式使用字符串而叼你。
小字符串优化sso: std::string name = “JKA”;
不会导致堆分配
标准库说了,小字符串,也就是不很长的字符串,它们不需要堆分配,而在栈的缓冲区上。VS2019中,任何小于15个字符的字符串都不会导致堆分配。
84 Track MEMORY ALLOCATIONS the Easy Way in C++
在堆上分配内存不是好的做法,尤其是性能关键的代码。
可以自己重载new 和 delete运算符实现,也可以使用VS内置的内存分配跟踪分析工具和 Valgrind 等。
Valgrind 是一款用于内存调试、内存泄露检测以及性能分析的软件开发工具。
85 lvalues and rvalues in C++
左值有地址和值,可以出现在赋值运算符左边或者右边。
右值只有值,只能出现在赋值运算符右边。
右值只有值,没有地址, 右值是一个优化技巧(C++),因为右值往往是临时变量的。
左值:有地址的值(located value)。
左值是有某种存储支持的变量,右值是临时值。
int i = 10;
左值绝大多数时间在等号左边,右值在右边。10是字面值(literal),没有存储空间。但左值也可以在等号右边:int a = i;
这里i为右值。
1 | int GetValue() { |
返回左值引用就可。
1 | int& GetValue() { // 返回左值引用 |
非const 不能用左值引用接受右值,const左值引用可以接受右值。
const左值引用既可以接收实际存在的左值变量,又可以兼容临时的右值。
1 | void SetValue(int& value) { } |
修改后:
1 |
|
只接受临时对象的函数,需要使用右值引用。右值引用和左值引用差不多,只不过使用两个&&。
1 |
|
用处:在移动语义中非常有用。这里的主要优势在于优化:如果知道传入的是一个临时对象的话,就不需要担心他们是否活着、是否完整、是否拷贝, 就能简单地偷它的资源,给到其他地方使用它们。
总结:
- 左值是有某种存储支持的变量,右值是临时值
- 左值引用仅接受左值,除非使用const;右值引用仅接受右值
86 Continuous Integration in C++
持续集成(CI)本质是构建自动化和测试。它可以帮助我们自动化整个过程,确保代码在所有平台和所有配置下都可以编译,然后运行一些基本的测试或任何你希望做的自动化测试,以确保一切是最佳的。
Jenkins 开源免费,可以在任何电脑上运行,但通常在服务器上。
87 Static Analysis in C++
PVS studio商业收费软件,用于静态分析(检查语法啥的)。
88 Argument Evaluation Order in C++
PrintSum(value++, value++);
未定义行为(正确答案),C++标准并没有定义这种情况下应该发生什么。
最佳的回答是:永远不要写这种自己都会问执行顺序是什么的代码!
89 Move Semantics in C++
C++11才引入右值引用,这是移动语义必需的。
移动语义让事情变得简单,移动语义本质上允许我们移动对象。
eg,如果把一个对象传递给一个函数,那么函数想要获得对象的所有权,别无选择只能拷贝——需要在当前堆栈帧中创建一个一次性对象(本身不需要其发生),然后复制到调用的函数中(改为移动对象效率更高,尤其是涉及堆内存分配)。
当想从函数返回一个对象时也是一样,仍然需要在函数中创建那个对象然后返回它(返回值优化可以解决此问题)。
= default
(C++11) It means that you want to use the compiler-generated version of that function, so you don’t need to specify a body.
= delete
pecify that you don’t want the compiler to generate that function automatically.
With the introduction of move constructors and move assignment operators, the rules for when automatic versions of constructors, destructors and assignment operators are generated has become quite complex. Using = default
and = delete
makes things easier as you don’t need to remember the rules: you just say what you want to happen.
1 |
|
简单来说就是做了一个浅拷贝,然后把拷贝置空(偷、接管了旧字符串——只能偷右值,右值不会再用)。
这部分建议阅读《C++ Primer》中完美转发部分的内容。
每个表达式都有两种特征:一是类型、二是值类别。很多人迷惑的右值引用为啥是个左值,那是因为右值引用是它的类型,左值是它的值类别。
想理解右值首先要先知道类型和值类别的区别;其次是各个值类别的定义是满足了某种形式它就是那个类别,经常说的能取地址就是左值,否则就是右值,这是定义之上的不严谨经验总结,换句话说,是左值还是右值是强行规定好的,你只需要对照标准看这个表达式满足什么形式就知道它是什么值类别了。
为什么要有这个分类,是为了语义,当一个表达式出现的形式表示它是一个右值,就是告诉编译器,我以后不会再用到这个资源,放心大胆的转移销毁,这就可以做优化,比如利用移动去“偷”内存,节省拷贝之类的。
move的作用是无条件的把表达式转成右值,也就是rvalue_cast,虽然编译器可以推断出左右值,但人有时比编译器“聪明”,人知道这个表达式的值以后我不会用到,所以可以在正常情况下会推成左值的地方强行告诉编译器这是个右值。
90 std::move and the Move Assignment Operator in C++
移动语义能够将一个对象移动到另一个对象,但是我们还没有涉及其中的2个关键:std::move
和move assignment operator
(移动赋值运算符)。
移动赋值操作符需要包含在类中,它将一个对象移动到一个现有类中。
C++三法则:如果需要析构函数,则一定需要拷贝构造函数和拷贝赋值操作符。
C++五法则:为了支持移动语义,增加了移动构造函数和移动赋值运算符。
1 |
|
VS中按F12可以转到定义
String dest (类型 变量名) 就是一个全新的对象,它正在被构造
赋值操作符,只有当我们把一个变量赋值给一个已定义的变量时才会被调用 dest = std::move(string) (不要声明并定义)