| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 开发工具 -> TheChernoCppTutorial -> 正文阅读 |
|
[开发工具]TheChernoCppTutorial |
文章目录
1. 欢迎来到C++使用C++的最大原因是直接控制硬件。用C++写的代码,这些代码被送去编译器去编译,这些编译器将代码输出为目标平台的机器码。机器码是你的设备在CPU上实际执行的指令。使用C++我们完全可以控制CPU执行的每一条指令。 C#和JAVA与cpp不同,是因为它们运行在虚拟机上,这意味着代码首先被编译成一种中间语言,当在目标平台运行应用程序时,虚拟机在程序运行时再转换成机器码。 C++是本地语言(native language)。比如x64编译器将输出x64机器代码,从而(也只能)在64位的CPU上运行。C++编译器位目标平台和架构生成机器码,编译后已经变成了平台上的机器语言,你只需要把机器代码指令放入CPU,CPU就会执行这些指令。 仅仅因为代码是本地的,并不意味着它会很快,垃圾代码甚至可能比虚拟机语言更慢比如C#或JAVA,因为它们倾向于运行时做系统优化。 5. C++是如何工作的以VS为例,配置Configurations只是构建项目时的一系列规则而已,解决方案平台Platforms是指你编译的代码的目标平台,Win32其实和X86是一样的东西。 项目中的每个CPP文件都会被编译,但头文件不会被编译。头文件的内容在预处理时包含进来了。 visual studio 单独编译一个文件:ctrl + F7 visual studio的error窗口,基本上就是分析output窗口,然后找到error这个单词,并抓取这部分信息展示出来,然后放入error list,所以建议看output窗口。 声明:这个符号、这个函数是存在的。 6. C++编译器是如何工作的C++编译器实际上需要做的唯一一件事,就是将我们的文本文件拿来转换他们,转换成一种称为目标文件格式的中间格式。这些obj文件可以传递到链接器,这个链接器可以做它所有的要链接的事情。 编译器在生成这些obj时实际上做了几件事: 每一个CPP文件将产生一个目标文件,这些CPP文件被称为翻译单元。本质上必须意识到C++不关心文件,文件不是存在于C++中的东西。在C++中,文件只是提供给编译器源代码的一种方式,你负责告诉编译器你输入的是什么类型的文件,以及编译器应该如何处理它。比如说把a.cpp改为b.hbh,只要告诉编译器这是个c++文件亦可,所以文件是没有意义的。 预处理到文件:(生成.i,但是就不生成.obj了)
要把运行时检查修改为default: 7. C++链接器是如何工作的现在链接的主要焦点是找到每个符号和函数在哪里,并把他们连接起来。应用程序需要知道入口点(entry point,一般为main)在哪里,当你实际运行你的应用程序的时候,C++运行时库(run time library)会说:这是main函数,我要跳到这里,然后开始执行代码。 在VS中按下ctrl + F7,只有编译会发生,链接将永远不会发生。但是如果是build或者是F5运行,它会编译然后链接。 语法错误(syntax error)以C打头(compile),告诉我们错误在编译阶段。LNK则代表链接link,告诉我们错误在链接阶段。 自定义entry point(一个exe一定有entry point): 当然,重复定义,会让链接器不知道链接到哪一个函数,因此也会出错。比如在一个头文件中定义了一个函数:
告诉我们Log函数已经在log.obj被定义了。 修复措施:
链接器需要带走我们所有的目标文件,并将它们链接在一起。它也将拉进我们可能用到的其他任何其他库,例如 C run time library、C++标准库、平台的api等等,从许多不同的地方linking是很常见的。 还有其他不同的链接:静态链接和动态链接(第49节课和第50节课,动态库dll静态库lib) 8. C++变量变量允许我们命名我们存储在内存中的数据(data),继续使用它。当创建一个变量时它将被存储在内存中——两个地方:堆和栈。 cherno解释变量的时候喜欢这样说:在C++中不同变量类型之间的唯一区别就是大小size(这个变量会占用多少内存)。这实际上是这些原始数据类型之间的唯一区别。 数据类型的实际大小取决于编译器——不同的编译器会有不同。由编译器确定类型的大小。 数据的大小(字节): char传统上用于存储字符,而不仅仅是数字(当然也可以存储数字)。同样也能对其他类型存储分配字符。因此数据类型的使用仅仅取决于程序员。 存小数: bool 1字节——因为在处理寻址内存时(addressing memory),也就是说我们需要从内存中找回我们的bool变量的值,我们没有办法去寻址只有以一个bit位的内容,我们只能寻址字节。因此我们不能创建只有一个bit位的变量,因为我们需要能够访问它,而我们现在只能访问字节。 因此我们可以聪明地用一字节存8个bool意义的量,比如bitset 操作符:sizeof,告诉我们是几字节的。因此sizeof(bool)就会打印出1,表示bool占用一个字节。 有了这些原始数据类型,之后还能转换为指针、引用。 9. C++函数函数就是我们写的代码块,被设计为用来执行特定的任务。在class中这些代码块则被称为方法method。 这里所说函数单独指类外的。 每次调用函数,编译器生成一个call指令(类外的,因此没有什么动态绑定,也暂时不考虑内联)。这基本上意义着,在一个运行的程序中,为了调用一个函数,我们需要创建一个堆栈结构,这意味着我们必须把像参数这样的东西推进堆栈。我们还需要一个叫做返回地址的东西压入堆栈。然后我们要做的是跳到二进制执行文件的不同部分,以便开始执行我们的函数指令。 而对于main函数,返回值是int,并且只有main函数可以不return——它会自动假设返回0.(这是现代C和C++的一个特性) 10. C++头文件像C#和Java就没有头文件的概念,我们实际上有两种不同的文件类型的概念: 就C++的基础而言,头文件通常用于声明某些类型的函数,以便它们可以被使用在你的程序中。 下面来说#pargma once 头文件保护符(监督、警卫)的东西——#ifndef #define #endif,在过去被广泛使用。但是现在我们有了这个新的预处理语句叫做pragma once. 几乎现在每个编译器都支持pragma once,所以它不止visual studio——GCC、Clang、MSVC都支持。 而有的include是<>有的是“”,原因是: 而像#include < iostream >,iostream其实是一个文件(虽然没有后缀。。)这是写C++标准库的人决定要这样做的。将C++标准库与C标准库进行区分。(有没有.h,如stdlib.h,这是一种区分C标准库和C++标准库的方法) 11. 如何在Visual Studio中调试代码断点和读取内存——这是调试的两大部分。当然会同时使用,换句话说你要设置断点就是为了读取内存。 断点break point是程序中调试器将中断的点。这里break的意思是暂停。当我们的程序执行到断点处时它将暂停。在我们这个例子的整个项目中它会挂起执行线程,让我们来看看这个程序的state。说到state,cherno指的是内存,我们可以暂停程序看看它的内存中发生了什么。 一个运行中的程序所需的内存是相当大的,包括你设置的每个变量、要调用的函数等等。当你将程序中断后,内存数据实际上还在,能查看内存对诊断你的程序出问题的原因非常有用。通过查看内存可以看到每一个变量的值。 确保在将会被执行的代码行打上断点(空行的断点不起作用)。 visual studio:
内存视图(VS中,alt + 6,或是debug-Windows-memory-memory1): 12. C++条件与分支(if语句)运行时检测汇编(debug时,鼠标右键->go to disassembly): 因为一个bool是一个字节,因此只要有东西在这个字节中不为0,这个bool就是true。 13. Visual Studio的最佳设置语言在 Tools->Options->Environment->International Settings 中修改: 平台和配置全选all: 所有的宏可以点这个编辑: 顺便一提,我们会经常用到宏 $(SolutionDir) ,其代表着与 sln 所在的路径,且自动在结尾有反斜杠\ vcxproj文件,我们的项目文件,它只是一个XML文件。sln文件实际上是一个文本文件夹,就像是某种目录。 16. C++指针对计算机来说内存就是一切。x86即32位地址为32位(8位16进制数,4 * 8 = 32),那么对应指针就是4字节;x64则为64位地址(16位16进制数),则对应指针8字节。 当你编写一个应用程序并启动它时,所有的程序都被载入到内存中,所有的指令告诉计算机在你写的代码中要做什么。所有这些都被加载到内存中。 指针是一个整数,一种存储内存地址的数字。 就是这样。 忘掉类型types,类型只是我们为了让生活更容易而创造的某种虚构,这都不重要。int*、double*或是class*,类型是完全无意义的,所有类型的指针都是保存内存地址的整数。 给指针类型type,我们只是说这个地址的数据被假设为我们给的类型,除此之外它没有任何意义。它只是一些我们在实际的源代码可以编写的东西,使我们的生活在语法层面上更容易。类型不会改变一个指针的实质。 所有void*这意味着我们现在不关心我们的代码中这个指针是什么类型的,因为我们只想保存一个地址。 类型types无关紧要,但类型对该内存的操作很有用,所以如果我想对它进行读写,类型可以帮助我因为编译器会知道例如一个int为4字节。(比如用*解引用的时候,我们对*ptr操作的时候我们就可以根据类型而知道对应着多大的内存) 17. C++引用根本上,引用通常只是指针的伪装,只是在指针上的语法糖(Syntactic sugar)。 比如 int& ,这里的&其实是类型的一部分。 19. C++类与结构体对比只有默认的可见性的区别:class默认private,struct默认public。 引入struct是为了让C++向后兼容C。 20. 如何写一个C++类cherno建议:类内私有成员变量前面可以加前缀 m_ ,比如 m_LogLevel 21. C++中的静态(static)配合知乎:https://zhuanlan.zhihu.com/p/394975612 static关键字在C++中有两个意思,这取决于上下文: 基本上直白来说,类外面的static,意味着你声明为static的符号,链接将只是在内部。这意味着它只能对你定义它的翻译单元可见(internal linkage)。然而类或结构体内部的静态变量(static)意味着该变量实际上将与类的所有实例共享内存。这意味着该静态变量在你在类中创建的所有实例中,静态变量只有一个实例。 类似的事情也适用于类中的静态方法。在类中没有实例会传递给该方法。 这一节着重讲在类和结构体外部的静态。 如果不用static定义全局变量,在别的翻译单元可以用extern int a这样的形式,这被称为 external linkage或external linking。 重点是,要让函数和变量标记为静态的,除非你真的需要它们跨翻译单元链接。 22. C++类和结构体中的静态(static)在几乎所有面向对象的语言里,static在一个类中意味着特定的东西。如果是static变量,这意味着在类的所有实例中,这个变量只有一个实例。比如一个entity类,有很多个entity实例,若其中一个实例更改了这个static变量,它会在所有实例中反映这个变化。这是因为即使创建了很多实例,static的变量仍然只有一个。正因如此,通过类实例来引用静态变量是没有意义的。因为这就像类的全局实例。 静态方法也是一样,无法访问类的实例。静态方法可以被调用,不需要通过类的实例。而在静态方法内部,你不能写引用到类实例的代码,因为你不能引用到类的实例。 比如一段最简单的测试代码:
于是我们需要给出定义,让链接器可以链接到合适的变量:
结果就是打印两个2,这是因为它们共享的是同一个变量,因此像上面那样写没有意义,最好写为: static作用到方法上也是一样的。静态方法不能访问非静态变量(毕竟不属于类实例,所以没有隐藏的this指针。即静态方法没有类实例)。
23. C++中的局部静态(Local Static)这一节讲述另一环境可能会找到static关键字:在一个局部作用域。 你可以在局部作用域中使用static来声明一个变量,这和前两种有所不同。这一种情况我们要考虑变量的生存期life time和变量的作用域scope。 生存期指的是变量实际存在的时间,而变量的作用域是指我们可以访问变量的范围。 静态局部(local static)变量允许我们声明一个变量,它的生存期基本相当于整个程序的生存期,然而它的作用范围被限制在这个作用域内。 例子: 另一个作用是单例:
可以参考:https://zhuanlan.zhihu.com/p/342769966 通过static静态,将生存期延长到永远。这意味着,我们第一次调用get的时候,它实际上会构造一个单例实例,在接下来的时间它只会返回这个已经存在的实例。
补充参考: 符号 (symbol) 是在 ELF 格式中会遇到的概念. 也就是在写汇编代码的时候会遇到, 而在更高级的语言 (比如 C 或 C++) 中不会直接遇到这个概念. 我们把讨论的范围限制在 Linux 上的 ELF 格式. 没有符号的东西没法放进内存, 所以也没法取地址, 没有办法 ODR-used. 常见的 Symbol Binding 类型有三种:
对于之前说的static,static一个函数,其实是生成了Local Symbol,
摘自https://zhuanlan.zhihu.com/p/380982475: 现在的 inline 语义大概就是: 允许同一个定义在不同的翻译单元出现, 但你需要确保不同翻译单元给出的定义是一致的. 可以看出, 最自然的实现方案就是使用 ELF 格式中的 Weak Symbol. 因为 inline 的语义变了, C++17 还引入了 inline variable. 也就是说, 不同的翻译单元中可以共用同一个符号作为变量. 在 C++ 之中, 除了 inline 会生成 Weak Symbol, 模板生成的内容也会 Weak Symbol. 所以模板可以放在头文件中, 而不用有担心重定义的错误. 事实上, 模板的定义也需要放在头文件中, 不然无法实例化. (除了显式实例化等情况) 24. C++枚举ENUM是enumeration的缩写。基本上它就是一个数值集合。不管怎么说,这里面的数值只能是整数。 默认从0开始,但是cherno习惯显示地写为0以增强可读性,比如:
这里的Level并不是真正的命名空间(与之对应的是C++11的强类型枚举 enum class,见我的笔记https://zhuanlan.zhihu.com/p/415508858) 28. C++虚函数虚函数引入了一种叫做 Dynamic Dispatch 动态联编 的东西,它通常通过虚函数表来实现编译。 cherno建议加override关键字,这更具可读性。 29. C++接口(纯虚函数)C++中的纯虚函数本质上与其他语言(如Java或C#)中的抽象方法或接口相同。 基本上纯虚函数允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数。 实际上,其他语言有interface关键字而不是叫class,但C++没有。接口只是C++的类而已。 30. C++可见性可见性是一个属于面向对象编程的概念,它指的是类的某些成员或方法实际上有多可见。这里说的可见性是指:谁能看到它们,谁能调用它们,谁能使用它们,所有这些东西。 可见性是对程序实际运行方式完全没有影响的东西,对程序性能或类似的东西也没影响。它纯粹是语言中存在的东西,让你能够写出更好的代码或者帮助你组织代码。 可见性是让代码更加容易维护,容易理解,不管是阅读代码还是扩展代码。这与性能无关,也不会产生完全不同的代码。可见性不是CPU需要理解的东西,这不是你的电脑需要知道的东西。它只是人类发明的东西,为了帮助其他人和自己。 比如可以让表示位置的成员变量x和y私有,然后搞两个函数SetX和SetY,这样我就可以在SetX函数中除了给x赋值,还可以顺便调用refresh去刷新显示器或是同步之类的。 C++中就三个:private、protected、public private:只有类和它的友元才能访问(继承的子类也不行)。 补充(关于继承)参考来源(https://www.cnblogs.com/lykkk/p/10699694.html) 31. C++数组C++数组就是表示一堆的变量组成的集合,一般是一行相同给类型的变量。 特别一提:内存访问违规(Memory access violation),在debug模式下,你会得到一个程序崩溃的错误消息,来帮助你调试那些问题;然而在release模式下,你可能不会得到报错信息。这意味着你已经写入了不属于你的内存。 循环的时候涉及到性能问题,我们一般是小于比较而不是小于等于(少一次等于的判断)。
上述代码中 *(ptr + 2) =6; 与 *(int*)((char*)ptr + 8) = 6; 是一样的,因为指针用operator+偏移,是按照指针指向类型的大小决定,修改为一个字节(char*),就是+8,之前的int*则是+2 计算数组中元素个数可以这样:
但是堆上分配的就不行,比如在VS2019的x64中测试:
输出结果就是5和2,后者的b为指针,64位即8字节,所以得到2. 如果是x86即win32,就是1 而栈上数组的大小需要是编译期常量,比如可以这样:
或是:
C++11中的std::array有一些优点如:边界检查,记录数组大小,比如:
32. C++字符串C++中有一种数据类型叫做char,是Character的缩写,这是一个字节的内存。它很有用因为它能把指针转换为char型指针,所以你可以用字节来做指针运算。它对于分配内存缓冲区也很有用,比如分配1024个char即1KB空间。它对字符串和文本也很有用,因为C++对待字符的默认方式是通过Ascii字符进行文本编码。我们在C++中处理字符是一个字符一个字节。Ascii可以扩展比如UTF-8、UTF-16、UTF-32,我们有wide string(宽字符串)等。我们有两个字节的字符、三个字节、四个字节的等等。 字符串实际上是字符数组,如一段很简单的程序: 比如: 我们正确的写法应该是:
显示地调用string的构造函数,再+一个const char数组,这会带来一些额外开销(拷贝),但是也没太大关系,
用string的运算符+=重载。 而用find去判断是否包含字符就可以这样:
最后,字符串复制实际上比较慢,如果可以就用const加引用方式传递。 33. C++字符串字面量字符串字面量就是双引号balabala,比如"hbh"
基本上,char是一个字节的字符,char16_t是两个字节的16个比特的字符(utf16),char32_t是32比特4字节的字符(utf32),const char就是utf8. 那么wchar_t也是两个字节,和char16_t的区别是什么呢?事实上宽字符的大小,实际上是由编译器决定的,可能是一个字节也可能是两个字节也可能是4个字节,实际应用中通常不是2个就是4个(Windows是2个字节,Linux是4个字节),所以这是一个变动的值。如果要两个字节就用char16_t,它总是16个比特的。 而在C++14中我们还能这样搞:
string_literals中定义了很多方便的东西,这里字符串字面量末尾加s,可以看到实际上是一个操作符函数,它返回标准字符串对象(std::string)
然后还有补充,如R,可以忽略转义字符,
34. C++中的CONSTconst首先作用于左边的东西;如果左边没东西,就做用于右边的东西 const被cherno称为伪关键字,因为它在改变生成代码方面做不了什么。有点像类和结构体的可见性。关键是,这是一个承诺,承诺一些东西是不变的,你是否遵守诺言取决于你自己。我们要保持const是因为这个承诺实际上可以简化很多代码。 并且常对象只能调用常函数,比如这样写就会报错: 记住,总是标记你的方法为const,如果它们实际上没有修改类或者它们不应该修改类。否则在有常量引用或类似的情况下就用不了你的方法。 而如果要修改别的变量,可以用关键字mutable: tips:
而:
中的y只是一个int型变量,非指针。 35. C++的mutable关键字mutable实际上有两种不同的用途:
第一种在34中已经讲过了,也是最主要的用法,下面来看第二种:
但是这样写很繁琐,就出现mutable关键字了,本质是一样的:
36. C++的成员初始化列表有一件要注意的事情:在成员初始化列表里需要按顺序写。这很重要,因为不管你怎么写初始化列表,它都会按照定义类的顺序进行初始化。
使用成员初始化列表,除了直观好看外,还有一个好处就是避开了一层性能浪费。如果是直接在构造函数中赋值,实际上的过程是先构造,之后再赋值。即以上图为例,二者区别是:
构造函数内赋值:
前者效率高一些,因为只调用了一次构造函数,而后者有两次构造函数加拷贝赋值。 因此能使用成员初始化列表就一定要使用。 37. C++的三元操作符实际上只是if的语法糖。 38. 创建并初始化C++对象一般栈的效率高,但是栈通常非常小,通常是1兆2兆,这取决于你的平台和编译器。因此有时候可能由于空间大小我们会在堆分配内存。 因此C++中我们有两种选择方式:
在C#中有一种叫做struct的东西,它是基于值的类型,他们实际上是在栈上分配的,即使你用了new关键字;但是在Java中所有东西都在堆上。C#中所有的类都是在堆上分配的。 cherno看到的一个最大的问题,就是每个来自Java或C#的托管语言的人,都会在C++中到处使用new关键字。简单来说就是性能问题,在堆上分配要比栈花费更长的时间,而且在堆上分配的话,你必须手动释放被分配的内存。 最后,如果对象太大,或是需要显示地控制对象的生存期,那就用堆上创建;否则就栈上分配吧,栈上创建简单多了,也更快。 39. C++ new关键字如果你来自Java或C#这样的托管语言,内存会自动清理。但在内存方面,你也没有那么多控制能力。 C++中,new一个对象,除了在堆中分配内存外,它还调用构造函数。 new 是一个操作符,就像加、减、等于一样。它是一个操作符,这意味着你可以重载这个操作符,并改变它的行为。 通常调用new会调用隐藏在里面的C函数malloc,但是malloc仅仅只是分配内存然后给我们一个指向那个内存的指针,而new还会调用构造函数。同样,delete则会调用destructor析构函数。 当我们使用new时,内存未释放,它没有被标记为释放,它不会被放回空闲列表,所以就不能再被new调用后再分配,直到我们调用delete,我们必须手动操作。 很多C++的策略可以让这个过程自动化,比如基于作用域的指针。也有一些高级策略比如引用计数。
所谓的placement new,这就是要决定前面的内存来自哪里,所以你并没有真正的分配内存。在这种情况下,你只需要调用构造函数,并在一个特定的内存地址中初始化你的Entity,可以通过些new()然后指定内存地址,比如
40. C++隐式转换与explicit关键字例子:
如上,在test4中,int型的21就被隐式转换为一个Entity对象。同时我们也能看到,对于语句 从cherno个人来说,他不会写 并且若构造函数写为explicit就会禁用这种隐式转换: 41. C++运算符及其重载运算符是我们使用给的一种符号,通常代替一个函数来执行一些事情。比如加减乘除、dereference运算符、箭头运算符、+=运算符、&运算符、左移运算符、new和delete、逗号、圆括号、方括号等等等等。 可以参考:https://en.cppreference.com/w/cpp/language/operators 运算符就是函数。运算符重载是一个非常有用的特性,但在Java等语言中不受支持,它在C#等语言中得到部分支持。C++给了我们完全的控制权。 在写库的时候,cherno喜欢函数和运算符重载这两种方法都写上,如下图: 43. C++的对象生存期(栈作用域生存期)每当我们在C++中进入一个作用域,我们是在push栈帧。它不一定非得是将数据push进一个栈帧。 栈上变量自动销毁,在很多方面都很有用,可以帮助我们自动化代码。比如类的作用域,比如像智能指针unique_ptr,这是一个作用域指针,或者像作用域锁(scoped_lock)。 但最简单的例子可能是作用域指针,它基本上是一个类,它是一个指针的包装器,在构造时用堆分配指针,然后在析构时删除指针,所以我们可以自动化这个new和delete。 例子:
可以看到,ScopedPtr就是我们写的一个最基本的作用域指针,由于其是在栈上分配的,然后作用域结束的时候,ScopedPtr这个类就被析构,析构中我们又调用delete把堆上的指针删除内存。 44. C++的智能指针智能指针本质上是原始指针的包装。当你创建一个智能指针,它会调用new并为你分配内存,然后基于你使用的智能指针,这些内存会在某一时刻自动释放。 unique_ptr是作用域指针,意味着超出作用域时,它会被销毁然后调用delete。
但是为了异常安全,一般我们使用std::make_unique shared_ptr实现的方式实际上取决于编译器和你在编译器中使用的标准库。
同样我们还能赋值给wek_ptr:
当你将一个shared_ptr赋值给另外一个shared_ptr,引用计数++,而若是把一个shared_ptr赋值给一个weak_ptr时,它不会增加引用计数。这很好,如果你不想要Entity的所有权,就像你可能在排序一个Entity列表,你不关心它们是否有效,你只需要存储它们的一个引用就可以了。 尽量使用unique_ptr因为它有一个较低的开销,但如果你需要在对象之间共享,不能使用unique_ptr的时候,就使用shared_ptr 46. C++的箭头操作符回到43我们所讲的ScopedPtr,这里我们可以进一步对其进行operator->的重载:
进一步我们可以写为const版本的:
然后还有一个我没有理解的诡异用法:
这样就取得了struct内部各个变量的偏移量:x为0,y为4(字节),z为8 当然还能写为 b站评论解释(https://www.bilibili.com/video/BV1X5411E77g?spm_id_from=333.999.0.0):
48. C++的stdvector使用优化实例代码:
可以看到运行结果: 我的环境是VS2019,x64,C++17标准,经过测试vector扩容因子为1.5,初始的capacity为0. 第一次push_back,capacity扩容到1,临时对象拷贝到真正的vertices所占内存中,第一次Copied;第二次push_back,发生扩容,capacity扩容到2,vertices发生内存搬移发生的拷贝为第二次Copied,然后再是临时对象的搬移,为第三次Copied;接着第三次push_back,capacity扩容到3(2*1.5 = 3,3之后是4,4之后是6…),vertices发生内存搬移发生的拷贝为第四和第五个Copied,然后再是临时对象的搬移为第六个Copied; 若是首先 而如果改为emplace_back,则是直接在vertices的内存中调用Vertex的构造函数,那自然没有临时对象的搬移,所以没有Copied。 那么emplace_back这要怎么做到呢,可以用placement new。 placement new允许我们将对象构建在已经分配的内存中,比如我写的一个示例:
并且,如果我们写的是 49. C++中使用库(静态链接)对于其他语言,比如Java、C#或python等,添加库是一项非常简单的任务。你可能用的是包管理器,也可能不是,但无论如何都是很简单的。 对于C++库,cherno倾向于在实际解决方案中的实际项目文件夹中,保留使用的库的版本。所以cherno实际上有那些物理二进制文件或代码的副本,这取决于在解决方案的实际工作目录中使用的方法。 对于大多数严肃的项目,cherno绝对推荐,实际构建源代码。如果是用VS,则可以添加另一个项目,该项目包含你的依赖库的源代码,然后将其编译为静态或动态库。 然而,如果拿不到源代码,或者这只是一个快速项目,不想花太多时间去设置,因为这是一种一次性的东西,或者只是一个不那么重要的项目,那么cherno可能倾向于链接二进制文件,因为它会更快更容易。 这一节将以二进制文件形式进行链接,而不是获取实际依赖库的源代码并自己进行编译。而在一个更加专业的大项目中,在有时间的地方,cherno肯定会自己编译它,因为它有助于调试,并且如果想修改库可以稍微改变一下。 比如以GLFW库为例: 库通常包含两部分:include和library(包含目录和库目录)。包含目录是一堆头文件。基本上include目录是一堆我们需要使用的头文件,这样我们就可以实际使用预构建的二进制文件中的函数,然后lib目录有那些预先构建的二进制文件。这里通常有两部分:dynamic library和static library。可以选择静态链接或动态链接(不是所有的库都提供了这两种方式) 静态链接意味着这个库会被放到你的可执行文件中,它在你的exe文件中,或者其他操作系统下的可执行文件。而动态链接库是在运行时被链接的,所以你仍然有一些链接,你可以选择在程序运行时装载动态链接库。有一个叫做loadLibrary的函数,你可以在WindowsAPI中使用它作为例子。它会载入你的动态库,可以从中拉出函数,然后开始调用函数。也可以在应用程序启动时加载你的dll文件,这就是动态链接库。最主要的区别就是库文件是否被编译到exe文件中或链接到exe文件中,还是只是一个单独的文件在运行时需要把它放在你的exe文件旁边或某个地方,然后你的exe文件可以加载它。 静态链接在技术上更快,因为编译器或链接器实际上可以执行链接时优化之类的。静态链接在技术上可以产生更快的应用程序。 我们有include files,然后还有库文件,两种文件都需要设置。
50. C++中使用动态库叫动态链接是因为链接发生在运行时,而静态链接是在编译时发生的。 当你编译一个静态库的时候,将其链接到可执行文件,也就是应用程序,或者链接到一个动态库。这有点像,你取出那个静态库的内容,然后你把这些内容放入到其他的二进制数据中。它实际上在你的动态库中或者在你的可执行文件中。正因为如此有很多优化可能会发生,因为编译器和链接器(特别是链接器)现在完全知道,静态链接时实际进入应用程序的代码。 对于Load-time Dynamic Linking,因为可执行文件知道动态链接库的存在(比如Windows弹出缺少balabala dll文件),可执行文件实际上把动态库作为一项需要,虽然动态库仍然是一个单独的文件、一个单独的模块,并且在运行时加载;对于Run-time Dynamic Linking,即也可以完全动态地加载动态库,这样可执行文件就与动态库完全没有任何关系了,你可以启动你的可执行文件,你的应用程序,它甚至不会要求你包含一个特定的动态库,但是在你的可执行文件中你可以写代码,去查找并在运行时加载某些动态库,然后获取函数指针或任何你需要的那个动态库中的东西,然后使用那个动态库。 对于动态库,其中之一是“静态的”动态库的版本,我的应用程序现场需要这个动态链接库,我已经知道里面有什么函数我可以使用什么;然后另一个版本是我想任意加载这个动态库,我甚至不知道里面有什么,但我想取出一些东西,或者我想用它做很多事情。 比如这里用前者,即应用程序现场需要这个动态链接库,我已经知道里面有什么函数我可以使用什么。 你可以在整个应用程序中设置库搜索位置,但是在可执行文件的根目录下也就是包含你的程序的目录,是一种自动搜索路径,如果把他们(即dll和exe)放到同一个文件夹里肯定没问题。 51. C++中创建与使用库(VisualStudio多项目)以我之前的dx12 YEngine的小项目为例: 然后在头文件中引入YEngine的文件夹,注意要相对路径:
52. C++中如何处理多返回值法一:传引用或者指针。cherno个人喜欢在前面添加前缀out,比如outA 法二:直接返回一个数组。当然这不通用,因为必须要同一种类型。 或者写为: 法三:tuple或pair 法四:定义一个结构体,然后返回 53. C++的模板模板有点像宏,它可以让你做很多事,然而泛型却非常受制于类型系统以及其他很多因素,模板templates要强大的多。 54. C++的堆与栈内存的比较在应用程序启动后,操作系统要做的就是:它会将整个程序加载到内存并分配一大堆物理ram以便使我们的应用程序可以运行。 栈和堆是ram中实际存在的两个区域:栈通常是一个预定义大小的内存区域,通常约为2兆字节左右;堆也是一个预定义了默认值的区域,但是它可以生长,并随着应用程序的进行而改变。重要的是要知道这两个区域的实际位置(物理位置)在我们的ram中是完全一样的。这两个内存区域的实际位置都在我们的内存中。
可以看到栈上分配的内存都挨着的,因为就是栈顶指针移动这么多字节。并且可以看到,更高的内存地址是第一个变量value,然后是array数组在其较低的地址,因为它是反向生长的。因为栈只是把东西堆在一起,所以很快。它就像一条cpu指令,我们所做的就是移动栈指针,然后我们返回栈指针的地址。 而对于栈,释放内存没有任何开销,因为栈释放内存与分配一样,不需要将栈指针反向移动然后返回栈指针地址,在这里我们只需要弹出栈中的东西,我们的栈指针自然就回到了作用域开始之前。一条CPU的删除指令就可以释放所有东西。 对于new关键字,其实际上调用了malloc(memory allocate的缩写),这样做通常会调用底层操作系统或平台的特定函数,这将在堆上为你分配内存。它这样做的方式是,当你启动你的应用时,你会得到一定数量的物理ram分配给你,你的程序会维护一个叫做空闲列表(free list)的东西,它是跟踪哪些内存块是空闲的,还有他们在哪里等等。所以当你使用malloc请求堆内存时,它可以浏览空闲列表,然后找到一块空闲内存至少和你要的一样大,我会给你们它的一个指针,然后还要记录比如分配的大小和它现在被分配的情况,有一堆记录要做。 因此,在堆上分配内存是一堆的事情,而在栈上分配内存,就像一条CPU指令。除此之外,栈上分配内存因为都是连续的,所以可以放在cpu缓存线上(Cache Line,可以理解为CPU Cache中的最小缓存单位)。因此在栈中分配可能不会得到cache miss而堆中分配则有可能(少量的cache miss没啥,大量就有点影响了)。因此它们之间最大的影响是分配的过程。 我们可以这样设置去观察汇编: 栈上分配: 55. C++的宏#开头的都是预处理器去处理的。 56. C++的auto关键字要注意的就是,要传引用时要在后面加&,比如 57. C++的静态数组(std array)用std::array的好处:我们可以通过.size()去访问它的大小,并且它是在栈上创建的。 58. C++的函数指针这一节讲的是原始风格的函数指针,来自于C语言。 函数指针,是将一个函数赋值给一个变量的方法。 示例:
一个更实际一点的例子: 59. C++的lambdalambda本质上是我们定义一种叫做匿名函数的方式。 只要你有一个函数指针,你都可以在C++中使用lambda,这就是它的工作原理。所以lambda是我们不需要通过函数定义就可以定义一个函数的方法。lambda的用法是,在我们会设置函数指针指向函数的任何地方,我们都可以将它设置为lambda 更改58讲的最后一个例子如下:
61. C++的名称空间在嵌套的名称空间中这样写蛮有效的:
namespace在特定的作用域内有效。 对于using namespace,你要尽量将这些限制在一个小的作用域下。如果可以比如说就在一个if内或者一个function内部写。永远不要在头文件里头用using namespace。 62. C++的线程https://zhuanlan.zhihu.com/p/415910318 一个示例代码(一直打印一行,直到我们按下回车才停止):
63. C++的计时C++11后有chrono,它是C++库的一部分,不需要去使用操作系统库。 但是在这之前如果想要一个非常精确的计时器,那么需要使用操作系统库。例如Windows中有QueryPerformanceCounter,事实上如果想要更多地控制计时,控制CPU的计时能力,那么你可能会使用平台特定的库。 然而这一讲主要看一看这种平台无关的C++标准库方法:
运行结果: std::endl因为某些原因非常慢,处于优化换成\n,并且包装一个结构体:
许多ide还有插码(instrumentation),可以用它来实际修改源代码,以包含某种分析工具,比如这个计时工具。 64. C++多维数组示例:
65. C++的排序示例代码:
运行结果: 66. C++的类型双关类型双关(type punning)只是一个花哨的术语,用来在C++中绕过类型系统。 C++是一个强类型语言,也就是说我们有一个类型系统。而像JavaScript就没有变量类型的概念。 然而,C++的这种类型系统并不像在其他语言中那样强制,比如Java,它们的类型很难绕开,包括C#也是,你虽然也可以绕开类型系统,但要做更多的工作。在C++中虽然类型是由编译器强制执行的,但你可以直接访问内存。 把一个int型的内存,换成double去解释,当然这样做很糟糕,因为添加了四字节不属于原本自己的内存,只是作为演示。
而如果只是想针对int的这四个字节,就可以用引用,而不是拷贝成一个新的变量:
还有一些演示的骚操作,比如: 67. C++的联合体共用内存。你可以像使用结构体或者类一样使用它们,你也可以给它添加静态函数或者普通函数、方法等待。然而你不能使用虚方法,还有其他一些限制。但通常人们用联合体来做的事情,是和类型双关紧密相关的。 通常union是匿名使用的,但是匿名union不能含有成员函数。 基本演示:
一个更实用点的例子:
68. C++的虚析构函数只要你允许一个类拥有子类,就一定要把析构函数写成虚函数,否则没人能安全地扩展这个类。
示例:
https://www.zhihu.com/question/268022905/answer/332152539
69. C++的类型转换类型转换 casting, type casting C++是强类型语言,意味着存在一个类型系统并且类型是强制的。
C++风格,四种主要的cast: 必须认识到的是,它们不做任何C风格类型转换不能做的事情。即它们可能会做其他的事情,但是实际的结果也只是一个成果的类型转换而已,C风格的强制转换可以实现所有这些(语法糖)。 在静态类型转换的情况下(static_cast),它们还会做一些其他的编译时检查,看看这种转换是否真的可能;reinterpret_cast则如单词reinterpret的重新解释意思一样,联系类型双关;const_cast,移除或者添加变量的const限定。 搞这么多cast的好处是,除了可能收到的那些编译时检查以外,还可以在代码库中搜索它们。 例子: 对于dynamic_cast,示例:
我们看到,有Derived和AnotherClass同时继承Base,通过 这时候用dynamic_cast,它做的就不仅是问这个问题,而且还会尝试去做转换,如果失败还会做一些事情。
比如这里,我们知道base其实是一个Derived指针,因此这里dynamic_cast<AnotherClass*>(base)会失败,得到的ac就会是nullptr,因此会打印This is the Derived。 所以dynamic_cast是一个很好的方法,来查看转换是否成功。它与运行时类型信息RTTI(runtime type information)紧密联系。它会做运行时检查。 const_cast是用来添加或移除const修饰符的,你可以用它来隐式添加const,但大部分情况下是用来移除const的:
因此若是写为:
则运行结果依然是3,因为被编译器优化了。 70. 条件与操作断点加了断点之后,注意这个conditions和actions: 比如可以这样写: 还能添加条件: 71. 现代C++中的安全以及如何教授C++里说的安全是什么意思? 安全编程,或者说是在编程中,我们希望降低崩溃、内存泄漏、非法访问等问题。 这一节重点讲讲指针和内存。
72. C++的预编译头文件以之前的YEngine的项目为例: 添加预编译头文件 没啥好说的,就是预编译成pch二进制文件以提升速度。唯一要提的就是,我在YEngine中添加了 pch.h与pch.cpp,貌似预编译后所有cpp文件都必须要包含 pch.h 才行,否则会报错。 pch.cpp设置: YEngine设置: 这里我其他cpp文件所包含的头文件我没有删,比如我在pch.h中已经包含Windows头文件了,但是我在GameTimer.cpp中仍然包含它: 因为我想反正头文件重复包含在 #pragma once 或是 #ifndef #define 中已经处理了,没几下的事,而我把它所依赖的写进来看的将会更加清晰,不知道理解的对不对,反正我这样做了。 73. C++的dynamic_castdynamic_cast是专门用于沿继承层次结构进行的强制类型转换。并且dynamic_cast只用于多态类类型。 在69节中曾说过:dynamic_cast与运行时类型信息RTTI(runtime type information)紧密联系。它会做运行时检查。它存储我们的所有类型的运行时类型信息,这是增加开销的,但它可以让你做动态类型转换之类的事情。 这里有两件事需要考虑: 我们也可以在代码中关闭运行时类型信息,如果我们不需要它的话:
不过还是这样更常见:
但要注意,动态强制转换确实会产生成本,所以如果你只想优化,如果你想要编写非常快的代码,你可能会想要避免这种情况。 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 | -2024/12/23 22:28:51- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |
数据统计 |