游戏客户端开发 —— C++ 学习
参考资料
- 《Hello 算法》:www.hello-algo.com/chapter_arr…
- 《C++ Primer》第五版——第三章 3.3 标准库类型 vector
- 数据结构与算法刷题教程:github.com/youngyangya…
- 力扣 Leetcode:leetcode.cn
- (可选-编程规范)学有余力时,可继续参考《C++ Core Guidelines》的相关章节:isocpp.github.io/CppCoreGuid… 、isocpp.github.io/CppCoreGuid…
- (可选-cheat sheets)学有余力时,可继续参考《hacking C++》:hackingcpp.com/cpp/std/seq…
- (可选-深入查阅)学有余力时,可继续参考《C++ reference》的相关章节:en.cppreference.com/w/cpp/conta…
- (可选-源码)微软 MSVC 对 C++ STL 的具体实现源码:github.com/microsoft/S… 。还可以直接用 Visual Studio 创建 C++ 项目,创建后新建 C++ 源文件,
#include<vector>,然后按住 Ctrl,点击 vector 查阅。
学习顺序
- 《Hello 算法》对应章节快速看一遍
- 学习《C++ Primer》里对应的标准介绍
- (可选)查阅 C++ Core Guidelines 里的相关内容
- 依据刷题教程刷题
学习记录
-
《Hello 算法》——数组、链表和列表的一些区别:
- 数组的英文是 array,列表的英文是 list。列表(list)是一个抽象的数据结构概念,它表示元素的有序集合。列表可以基于链表或数组实现。
- 数组和链表分别代表了“连续存储”和“分散存储”两种物理结构。总体而言,数组具有更高的缓存命中率,因此它在操作效率上通常优于链表。这使得在解决算法问题时,基于数组实现的数据结构往往更受欢迎。必要使用链表的情况主要是二叉树和图。 栈和队列往往会使用编程语言提供的
stack和queue,而非链表。
物理结构在很大程度上决定了程序对内存和缓存的使用效率,进而影响算法程序的整体性能。
-
《Hello 算法》——动态数组:
- 动态数组本质上还是数组。
- 动态数组(dynamic array) 是许多编程语言中的标准库提供的列表的实现方式。例如 Python 中的
list、Java 中的ArrayList、C++ 中的vector和 C# 中的List等。 - 动态数组中有三个重点概念:初始容量、数量记录(用于记录当前元素数量)和扩容机制。若插入元素时列表容量已满,则需要进行扩容。先根据扩容倍数创建一个更大的数组,再将当前数组的所有元素依次移动至新数组,最差时间复杂度为 ;若插入元素时列表容量未满且要插入列表的尾部,则可以依据数量记录,将元素直接插入动态数组的尾部,最差时间复杂度为 ;若插入元素时列表容量未满且不是插入列表的尾部,则最差时间复杂度为 。
-
《C++ Core Guidelines》——The Standard Library:
- SL.con.1: 优先使用 STL 的
array或vector而不是 C 数组- C 数组不够安全,相比
array和vector没有优势。 - 对于固定长度的数组,使用
std::array,因为它在传递给函数时不会退化为指针,并且知道自己的大小。此外,与内置数组一样,栈分配的std::array将其元素保存在栈上。 - 对于可变长度的数组,使用
std::vector,它可以更改大小并处理内存分配。 - 注意:将分配在栈上的固定大小数组与其元素位于自由存储区(堆)的
vector进行性能比较是不合理的,因为这两者的用途不同。这样的比较就像是将栈上的std::array与通过malloc()在堆上分配的内存进行比较一样,没有意义。对于大多数代码来说,栈分配和堆分配的性能差异并不显著,但vector的便利性和安全性是重要的。那些编写对这种差异敏感的代码的人,完全有能力在array和vector之间做出选择。
对于某些性能敏感的应用程序,比如游戏开发或者实时系统,栈分配和堆分配之间的差异可能会变得重要。 栈分配的速度通常比堆分配快,因为栈分配只需要移动栈指针,而堆分配需要在内存中寻找一个足够大的空闲块。 此外,频繁的堆分配和释放可能会导致内存碎片,进一步影响性能。
- C 数组不够安全,相比
- SL.con.2: 默认优先使用 STL
vector,除非有理由使用其他容器vector和array是唯一提供以下优势的标准容器:- 最快的一般用途访问(随机访问,包括向量化友好)
- 默认的最快访问模式(从头到尾或从尾到头对预取器友好)
- 最低的空间开销(连续布局没有每个元素的开销,对缓存友好)
- 通常你需要向容器添加和删除元素,因此默认使用
vector; 如果不需要修改容器的大小,使用array。. - 注意:
string字符串不应作为单个字符的容器。string字符串是文本字符串;如果你需要字符容器,请使用vector</*char_type*/>或array</*char_type*/>。 - 例外:- 如果你想要一个保证 O(1) 或 O(log N) 查找的字典样式查找容器,该容器会更大(超过几 KB),并且你经常进行插入操作,使得维护一个排序的
vector的开销不可接受,请继续使用unordered_map或map。
- SL.con.3: 避免越界错误
- 适用于元素范围的标准库函数都有(或可以有)使用
span的边界安全重载。标准类型如vector可以在边界配置文件下进行边界检查(以兼容方式,例如通过添加契约),或与at()一起使用。 - 理想情况下,应静态强制执行边界内保证。例如:
range-for不能循环超出其应用的容器范围v.begin(), v.end()很容易确定是边界安全的 这样的循环和任何未经检查/不安全的等效代码一样快。
- 示例:如果代码使用未修改的标准库,则仍有解决方法使
std::array和std::vector以边界安全的方式使用。代码可以在每个类上调用.at()成员函数,这将导致抛出std::out_of_range异常。或者,代码可以调用at()自由函数,这将在边界违规时快速失败(或自定义操作)。-
void f(std::vector<int>& v, std::array<int, 12> a, int i) { v[0] = a[0]; // 错误 v.at(0) = a[0]; // 正确(替代方案 1) at(v, 0) = a[0]; // 正确(替代方案 2) v.at(0) = a[i]; // 错误 v.at(0) = a.at(i); // 正确(替代方案 1) v.at(0) = at(a, i); // 正确(替代方案 2) }
-
- 适用于元素范围的标准库函数都有(或可以有)使用
- SL.con.1: 优先使用 STL 的
-
《C++ Primer》——箭头运算符(->)
- 箭头运算符的出现是为了简化解引用和成员访问这套组合操作。例子:如果没有箭头运算符,则代码是
(*it).mem,而采用箭头运算符后,则为it->mem。
- 箭头运算符的出现是为了简化解引用和成员访问这套组合操作。例子:如果没有箭头运算符,则代码是
-
《Hacking cpp》—— vector 对象在内存上的分布
- 注意到,大部分情况下,vector 对象 w 的指针存储在栈区,而其所指向的数据存储在堆区。 更多信息可参考:hackingcpp.com/cpp/lang/me…