240726 学习笔记—— C++ 数组、vector 容器

256 阅读6分钟

游戏客户端开发 —— C++ 学习

参考资料

  1. 《Hello 算法》:www.hello-algo.com/chapter_arr…
  2. 《C++ Primer》第五版——第三章 3.3 标准库类型 vector
  3. 数据结构与算法刷题教程:github.com/youngyangya…
  4. 力扣 Leetcode:leetcode.cn
  5. (可选-编程规范)学有余力时,可继续参考《C++ Core Guidelines》的相关章节:isocpp.github.io/CppCoreGuid…isocpp.github.io/CppCoreGuid…
  6. (可选-cheat sheets)学有余力时,可继续参考《hacking C++》:hackingcpp.com/cpp/std/seq…
  7. (可选-深入查阅)学有余力时,可继续参考《C++ reference》的相关章节:en.cppreference.com/w/cpp/conta…
  8. (可选-源码)微软 MSVC 对 C++ STL 的具体实现源码:github.com/microsoft/S… 。还可以直接用 Visual Studio 创建 C++ 项目,创建后新建 C++ 源文件,#include<vector>,然后按住 Ctrl,点击 vector 查阅。

学习顺序

  1. 《Hello 算法》对应章节快速看一遍
  2. 学习《C++ Primer》里对应的标准介绍
  3. (可选)查阅 C++ Core Guidelines 里的相关内容
  4. 依据刷题教程刷题

学习记录

  1. 《Hello 算法》——数组、链表和列表的一些区别:

    • 数组的英文是 array,列表的英文是 list。列表(list)是一个抽象的数据结构概念,它表示元素的有序集合。列表可以基于链表或数组实现。
    • 数组和链表分别代表了“连续存储”和“分散存储”两种物理结构。总体而言,数组具有更高的缓存命中率,因此它在操作效率上通常优于链表。这使得在解决算法问题时,基于数组实现的数据结构往往更受欢迎。必要使用链表的情况主要是二叉树和图。 栈和队列往往会使用编程语言提供的 stack 和 queue ,而非链表。

    物理结构在很大程度上决定了程序对内存和缓存的使用效率,进而影响算法程序的整体性能。

  2. 《Hello 算法》——动态数组:

    • 动态数组本质上还是数组。
    • 动态数组(dynamic array) 是许多编程语言中的标准库提供的列表的实现方式。例如 Python 中的 list 、Java 中的 ArrayList 、C++ 中的 vector 和 C# 中的 List 等。
    • 动态数组中有三个重点概念:初始容量、数量记录(用于记录当前元素数量)和扩容机制。若插入元素时列表容量已满,则需要进行扩容。先根据扩容倍数创建一个更大的数组,再将当前数组的所有元素依次移动至新数组,最差时间复杂度为 O(n)O(n);若插入元素时列表容量未满且要插入列表的尾部,则可以依据数量记录,将元素直接插入动态数组的尾部,最差时间复杂度为 O(1)O(1);若插入元素时列表容量未满且不是插入列表的尾部,则最差时间复杂度为 O(n)O(n)
  3. 《C++ Core Guidelines》——The Standard Library:

    • SL.con.1: 优先使用 STL 的 arrayvector 而不是 C 数组
      • C 数组不够安全,相比 arrayvector 没有优势。
      • 对于固定长度的数组,使用 std::array,因为它在传递给函数时不会退化为指针,并且知道自己的大小。此外,与内置数组一样,栈分配的 std::array 将其元素保存在栈上。
      • 对于可变长度的数组,使用 std::vector,它可以更改大小并处理内存分配。
      • 注意:将分配在栈上的固定大小数组与其元素位于自由存储区(堆)的vector进行性能比较是不合理的,因为这两者的用途不同。这样的比较就像是将栈上的std::array与通过malloc()在堆上分配的内存进行比较一样,没有意义。对于大多数代码来说,栈分配和堆分配的性能差异并不显著,但vector的便利性和安全性是重要的。那些编写对这种差异敏感的代码的人,完全有能力在arrayvector之间做出选择。

      对于某些性能敏感的应用程序,比如游戏开发或者实时系统,栈分配和堆分配之间的差异可能会变得重要。 栈分配的速度通常比堆分配快,因为栈分配只需要移动栈指针,而堆分配需要在内存中寻找一个足够大的空闲块。 此外,频繁的堆分配和释放可能会导致内存碎片,进一步影响性能。

    • SL.con.2: 默认优先使用 STL vector,除非有理由使用其他容器
      • vectorarray 是唯一提供以下优势的标准容器:
        • 最快的一般用途访问(随机访问,包括向量化友好)
        • 默认的最快访问模式(从头到尾或从尾到头对预取器友好)
        • 最低的空间开销(连续布局没有每个元素的开销,对缓存友好)
      • 通常你需要向容器添加和删除元素,因此默认使用 vector 如果不需要修改容器的大小,使用 array。.
      • 注意:string 字符串不应作为单个字符的容器。string 字符串是文本字符串;如果你需要字符容器,请使用 vector</*char_type*/>array</*char_type*/>
      • 例外:- 如果你想要一个保证 O(1) 或 O(log N) 查找的字典样式查找容器,该容器会更大(超过几 KB),并且你经常进行插入操作,使得维护一个排序的 vector 的开销不可接受,请继续使用 unordered_mapmap
    • SL.con.3: 避免越界错误
      • 适用于元素范围的标准库函数都有(或可以有)使用 span 的边界安全重载。标准类型如 vector 可以在边界配置文件下进行边界检查(以兼容方式,例如通过添加契约),或与 at() 一起使用。
      • 理想情况下,应静态强制执行边界内保证。例如:
        • range-for 不能循环超出其应用的容器范围
        • v.begin(), v.end() 很容易确定是边界安全的 这样的循环和任何未经检查/不安全的等效代码一样快。
      • 示例:如果代码使用未修改的标准库,则仍有解决方法使 std::arraystd::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)
            }
          
  4. 《C++ Primer》——箭头运算符(->)

    • 箭头运算符的出现是为了简化解引用和成员访问这套组合操作。例子:如果没有箭头运算符,则代码是(*it).mem,而采用箭头运算符后,则为it->mem
  5. 《Hacking cpp》—— vector 对象在内存上的分布

    • image.png
    • 注意到,大部分情况下,vector 对象 w 的指针存储在栈区,而其所指向的数据存储在堆区。 更多信息可参考:hackingcpp.com/cpp/lang/me…