Cpp学习20 第 16 章 动态数组:std::vector

铁名_IronName Lv5

16.1 — 容器和数组简介

当你去超市买一打鸡蛋时,你(很可能)不会一个个地挑选鸡蛋放进购物车(肯定不会吧?)。相反,你很可能会选择一盒鸡蛋。鸡蛋盒是一种容器,里面装着一定数量的鸡蛋(通常是6个、12个或24个)。再想想早餐麦片,里面有很多小块的麦片。你肯定不会想把这些麦片一块一块地放在食品柜里吧!麦片通常装在盒子里,盒子也是一种容器。我们在日常生活中经常使用容器,因为它们方便我们管理各种物品。
容器在编程中也存在,它简化了对象集合(可能非常庞大)的创建和管理。在一般编程中, 容器是一种数据类型,用于存储一组未命名的对象(称为元素 )。

你已经在使用一种容器类型了:字符串!字符串容器用于存储字符集合,然后可以将这些字符作为文本输出。

在编程中,容器中元素的数量通常称为其长度 (或有时称为计数 )。

在 C++ 中,术语 size 也常用于表示容器中的元素数量。这是一种不太恰当的命名选择,因为术语“size”也可以指对象使用的内存字节数(由 sizeof 运算符返回)。我们将使用“长度”一词来指代容器中的元素数量,并使用“大小”一词来指代对象所需的存储空间。

元素类型

在大多数编程语言(包括 C++)中,容器是同质的 ,这意味着容器中的元素必须具有相同的类型。

C++中的容器

Containers 库是 C++ 标准库的一部分,其中包含各种实现常见容器类型的类。实现容器的类有时被称为容器类 。

以下类型在通用编程定义中属于容器,但 C++ 标准不将其视为容器:

  • C-style arrays  C 风格数组
  • std::string
  • std::vector<bool>

数组简介

arrays
数组是一种容器数据类型,它以连续的方式存储一系列值(这意味着每个元素都放置在相邻的内存位置,没有空隙)。数组允许快速、直接地访问任何元素。

C++ 包含三种主要数组类型:(C 风格的)数组、 std::vector 容器类和 std::array 容器类。

16.2 — std::vector 和 列表构造函数简介

std::vector 是 C++ 标准容器库中实现数组的容器类之一。std::vector 在头文件中定义为类模板,带有一个模板类型参数,用于定义元素的类型。因此, std::vector<int> 声明了一个元素类型为 int 的 std::vector 。

实例化 std::vector 对象非常简单:

1
2
3
4
5
6
7
8
9
#include <vector>

int main()
{
// Value initialization (uses default constructor)
std::vector<int> empty{}; // vector containing 0 int elements

return 0;
}

使用值列表初始化 std::vector

1
2
3
4
5
6
7
8
9
10
#include <vector>

int main()
{
// List construction (uses list constructor)
std::vector<int> primes{ 2, 3, 5, 7 }; // vector containing 4 int elements with values 2, 3, 5, and 7
std::vector vowels { 'a', 'e', 'i', 'o', 'u' }; // vector containing 5 char elements with values 'a', 'e', 'i', 'o', and 'u'. Uses CTAD (C++17) to deduce element type char (preferred).

return 0;
}

使用下标运算符(运算符[])访问数组元素

在 C++ 中,访问数组元素最常用的方法是使用数组名加上下标运算符( operator[] )。要选择特定元素,需要在下标运算符的方括号内提供一个整数值来标识要选择的元素。这个整数值称为下标 (或非正式地称为索引 )。就像我们的邮箱一样,第一个元素使用索引 0 访问,第二个元素使用索引 1 访问,依此类推……

operator[] 不进行任何边界检查 ,也就是说,它不会检查索引是否在 0 到 N-1(含 0 和 N-1)的范围内。向 operator[] 传递无效索引将导致未定义行为。

数组在内存中是连续的。

数组是少数几种允许随机访问的容器类型之一,这意味着可以直接访问容器中的任何元素(与顺序访问不同,顺序访问中元素必须按特定顺序访问)。随机访问数组元素通常效率很高,这使得数组非常易于使用。这也是数组通常优于其他容器的主要原因之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Copy init
std::vector<int> v1 = 10; // 10 not an initializer list, copy init won't match explicit constructor: compilation error

// Direct init
std::vector<int> v2(10); // 10 not an initializer list, matches explicit single-argument constructor

// List init
std::vector<int> v3{ 10 }; // { 10 } interpreted as initializer list, matches list constructor

// Copy list init
std::vector<int> v4 = { 10 }; // { 10 } interpreted as initializer list, matches list constructor
std::vector<int> v5({ 10 }); // { 10 } interpreted as initializer list, matches list constructor

// Default init
std::vector<int> v6 {}; // {} is empty initializer list, matches default constructor
std::vector<int> v7 = {}; // {} is empty initializer list, matches default constructor

列表初始化器通常用于允许我们使用元素值列表来初始化容器,并且应该用于此目的。大多数情况下,这正是我们所需要的。因此,如果 10 的是元素值,那么 { 10 } 是合适的。如果 10 的是容器的非列表构造函数的参数,则应使用直接初始化。

  • 当你想指定“元素数量”时:坚决只用圆括号 ()。比如 vector<int> v(10, 5);(10个5)。
  • 当你想指定“具体元素值”时:才用花括号 {}。比如 vector<int> v{1, 2, 3};

Const 和 constexpr std::vector

const std::vector 必须先初始化,初始化后不能修改。此 vector 的元素被视为 const 类型。
 std::vector<const int> 是不允许的

16.3 — std::vector 和无符号长度及下标问题

std::vector and the unsigned length and subscript problem

容器长度符号问题

略。结论是:历史遗留问题,只能接受。

std::vector 的长度和索引具有 size_type 类型。

获取 std::vector 的长度

可以使用 size() 成员函数(该函数以 unsigned size_type 形式返回长度)来询问容器类对象的长度。
在 C++17 中,我们还可以使用 std::size() 非成员函数(对于容器类,它只是调用 size() 成员函数)。

如果想使用上述任一方法将长度存储在有符号类型的变量中,则很可能会导致有符号/无符号类型转换警告或错误。最简单的解决方法是使用 static_cast 将结果转换为所需的类型。

使用 std::ssize() 获取 std::vector 的长度 (C++20)
它以大型 有符号 整数类型(通常是 std::ptrdiff_t ,该类型通常用作 std::size_t 有符号对应类型)返回长度。

使用 at() 成员函数访问数组元素

使用 at() 成员函数访问数组元素时会进行运行时边界检查。
由于 at() 每次调用都会进行运行时边界检查,因此它比 operator[] 慢(但更安全)。

尽管不安全,我们还是优先用 operator[],只要在使用前检查索引就好。

当使用 constexpr(有符号)int 对 std::vector 进行索引时,我们可以让编译器隐式地将其转换为 std::size_t 而无需进行缩减转换。

使用非 constexpr 值对 std::vector 进行索引

有很多方法可以避免这个问题(例如,每次索引数组时都使用 static_cast 将 int 强制转换为 std::size_t ),但所有这些方法最终都不可避免地会使代码变得混乱或复杂。最简单的做法是使用一个 std::size_t 类型的变量作为索引,并且只将其用于索引。这样就可以从一开始就避免任何非上下文相关的类型转换。

另一个不错的替代方案是,不要直接索引 std::vector 本身,而是索引 data() 成员函数的结果。data() 成员函数返回一个指向这个底层 C 风格数组的指针,我们可以对其进行索引。由于 C 风格数组允许使用有符号和无符号类型进行索引,因此我们不会遇到任何符号转换问题。

16.4 — 传递 std::vector

传递不同元素类型的 std::vector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <vector>

template <typename T>
void passByRef(const std::vector<T>& arr)
{
std::cout << arr[0] << '\n';
}

int main()
{
std::vector primes{ 2, 3, 5, 7, 11 };
passByRef(primes); // ok: compiler will instantiate passByRef(const std::vector<int>&)

std::vector dbl{ 1.1, 2.2, 3.3 };
passByRef(dbl); // ok: compiler will instantiate passByRef(const std::vector<double>&)

return 0;
}

使用通用模板或简写函数模板传递 std::vector,略,看起来不太安全。

断言数组长度

避免在需要断言数组长度的情况下使用 std::vector 。使用支持 constexpr 数组的类型(例如 std::array )可能更好,因为你可以对 constexpr 数组的长度 static_assert 。

16.5 — 返回 std::vector,以及移动语义简介

移动语义 move semantics

Copy semantics  复制语义

复制语义指的是决定对象副本创建方式的规则。当我们说某个类型支持复制语义时,意味着该类型的对象是可复制的,因为创建此类副本的规则已经定义。当我们说正在调用复制语义时,意味着我们已经执行了会创建对象副本的操作。

移动语义

如果有一种方法可以让 arr2 “窃取”临时对象的数据而不是复制它呢?这样, arr2 就成为了数据的新所有者,无需再复制数据。当数据的所有权从一个对象转移到另一个对象时,我们称之为数据移动 。这种移动的成本通常微不足道(通常只需要两到三次指针赋值,这比复制数据数组快得多!)。
此外,还有一个好处是,当临时对象在表达式结束时被销毁时,它将不再有任何数据需要销毁,因此我们也不必支付这部分成本。

这就是移动语义的本质,它指的是决定如何将数据从一个对象移动到另一个对象的规则。当调用移动语义时,任何可以移动的数据成员都会被移动,任何不能移动的数据成员都会被复制。移动数据而非复制数据的能力使得移动语义比复制语义更高效,尤其是在我们可以用低成本的移动操作来替代成本高昂的复制操作时。

但是,当以下所有条件都成立时,将调用移动语义:

  • 该对象类型支持移动语义。
  • 该对象正在使用相同类型的右值(临时)对象进行初始化(或赋值)。
  • 这一举动并未被省略。
    支持移动语义的类型并不多。不过, std::vector 和 std::string 都支持!

由于按值返回会返回一个右值,如果返回的类型支持移动语义,那么返回值就可以被移动到目标对象中,而不是被复制到目标对象中。这使得按值返回对于这些类型来说成本极低!

复制成本高的类型不应该按值传递,但如果它们可以移动,就可以按值返回。

16.6 — 数组和循环

按某种顺序访问容器中的每个元素称为遍历 ,或遍历容器。遍历通常也称为迭代 ,或遍历 容器 。

模板、数组和循环 解锁可扩展性

数组提供了一种存储多个对象而无需为每个元素命名的方法。
循环提供了一种遍历数组的方法,而无需显式列出每个元素。
模板提供了一种对元素类型进行参数化的方法。

模板、数组和循环结合起来,使我们能够编写可以操作元素容器的代码,而无需考虑容器中的元素类型或元素数量!

16.7 — 数组、循环和符号挑战 解决方案

首先,只要 index >= 0 (或者说,只要 index 为正数),循环就会一直执行,而当 index 为无符号数时,这个条件总是成立的。因此,循环永远不会终止。
其次,当索引值为 0 时,如果我们递减 index ,它会回绕到一个很大的正值,然后我们在下一次迭代中用这个正值作为数组的索引。这会导致索引越界,从而造成未定义行为。如果vector 为空,也会遇到同样的问题。

这里有很多可行的方案,我们将按照我们认为从最差到最好的顺序逐一介绍。你很可能会在其他人编写的代码中遇到所有这些方案。

关闭有符号/无符号转换警告

这是最简单的解决方案,但我们不建议这样做,因为这也会抑制生成合法的符号转换警告,如果不加以解决,可能会导致错误。

使用无符号循环变量

因为 size_type 几乎总是 size_t 的类型定义,所以很多程序员干脆完全跳过使用 size_type ,直接使用更容易记忆和输入的 std::size_t :

1
for (std::size_t index { 0 }; index < arr.size(); ++index)

除非你使用自定义分配器(而你很可能没有),否则我们认为这是一个合理的方法。

使用有符号的循环变量

这里有三个(有时是四个)不错的选择。

  1. 除非你处理的是非常大的数组,否则使用 int 类型应该没问题(尤其是在 int 类型占用 4 个字节的架构 int )。int 是默认的有符号整数类型,当我们不太关心数据类型时,我们通常使用它,而在这里几乎没有理由使用其他类型。
  2. 如果您要处理非常大的数组,或者如果您想更谨慎一些,可以使用名称略显奇怪的 std::ptrdiff_t 。此 typedef 通常用作 std::size_t 的有符号对应类型。
  3. 由于 std::ptrdiff_t 名称很奇怪,另一种好方法是为索引定义自己的类型别名:
1
2
3
4
using Index = std::ptrdiff_t;

// Sample loop using index
for (Index index{ 0 }; index < static_cast<Index>(arr.size()); ++index)
  1. 如果可以从初始化器中推导出循环变量的类型,则可以使用 auto 让编译器推断类型:
1
for (auto index{ static_cast<std::ptrdiff_t>(arr.size())-1 }; index >= 0; --index)

获取数组的长度(以有符号值表示)

在 C++20 之前,最佳选择是将 size() 成员函数或 std::size() 的返回值 static_cast 转换为有符号类型。

通过将长度计算移出循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
#include <vector>

using Index = std::ptrdiff_t;

int main()
{
std::vector arr{ 9, 7, 5, 3, 1 };

auto length{ static_cast<Index>(arr.size()) };
for (auto index{ length-1 }; index >= 0; --index)
std::cout << arr[static_cast<std::size_t>(index)] << ' ';

return 0;
}

在 C++20 中,使用 std::ssize() 。

将有符号循环变量转换为无符号索引

  1. 使用名称简短的转换函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <type_traits> // for std::is_integral and std::is_enum
#include <vector>

using Index = std::ptrdiff_t;

// Helper function to convert `value` into an object of type std::size_t
// UZ is the suffix for literals of type std::size_t.
template <typename T>
constexpr std::size_t toUZ(T value)
{
// make sure T is an integral type
static_assert(std::is_integral<T>() || std::is_enum<T>());

return static_cast<std::size_t>(value);
}

int main()
{
std::vector arr{ 9, 7, 5, 3, 1 };

auto length { static_cast<Index>(arr.size()) }; // in C++20, prefer std::ssize()
for (auto index{ length-1 }; index >= 0; --index)
std::cout << arr[toUZ(index)] << ' '; // use toUZ() to avoid sign conversion warning

return 0;
}
  1. 使用自定义视图
    将用到教程提供的 SignedArrayView.h。

这个索引类型的无符号 事也太多了吧。

改为对底层 C 风格数组进行索引

调用 data() 成员函数并对其进行索引。由于 data() 返回的是 C 风格数组,而 C 风格数组允许使用有符号和无符号值进行索引,因此可以避免符号转换问题。

唯一明智的选择:完全避免索引!

C++ 提供了其他几种遍历数组的方法,这些方法完全不使用索引。如果没有索引,我们就不会遇到所有这些有符号/无符号转换问题。两种常用的无索引数组遍历方法包括基于范围的 for 循环和迭代器。

如果你只是使用索引变量来遍历数组,那么最好选择不使用索引的方法。

16.8 — 基于范围的 for 循环(for-each)

由于遍历数组(向前遍历)非常常见,C++ 支持另一种 for 循环,称为基于范围的 for 循环 (有时也称为 for-each 循环 ),它允许在无需显式索引的情况下遍历容器。基于范围的 for 循环更简单、更安全,并且适用于 C++ 中所有常见的数组类型(包括 std::vector 、 std::array 和 C 风格的数组)。

基于范围的 for 循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <vector>

int main()
{
std::vector fibonacci { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };

for (int num : fibonacci) // iterate over array fibonacci and copy each value into `num`
std::cout << num << ' '; // print the current value of `num`

std::cout << '\n';

return 0;
}

遍历容器时,优先使用基于范围的 for 循环,而不是普通的 for 循环。

使用基于范围的 for 循环进行类型推断( auto ),可以让编译器推断数组元素的类型。
使用 auto 的另一个好处是,如果数组的元素类型发生更新(例如从 int 到 long ), auto 将自动推断更新后的元素类型,确保它们保持同步(并防止发生类型转换)。

使用引用避免复制元素

1
2
3
4
std::vector<std::string> words{ "peter", "likes", "frozen", "yogurt" };

for (const auto& word : words) // word is now a const reference
std::cout << word << ' ';

 word 现在是一个常量引用。每次循环迭代时, word 都会绑定到下一个数组元素。这样我们就可以在不进行代价高昂的复制操作的情况下访问数组元素的值。

通常情况下,对于复制成本低的类型,我们会使用 auto 对于需要修改元素的类型,我们会使用 auto& 而对于复制成本高的类型,我们会使用 const auto& 。但是,对于基于范围的 for 循环,许多开发者认为最好始终使用 const auto& 因为它更具前瞻性。

最佳实践

对于基于范围的 for 循环,建议将元素类型定义为:

  • 想要修改元素的副本时,请使用 auto 。
  • 想要修改原始元素时,auto&
  • 否则 const auto& (只需要查看原始元素时)。

获取当前元素的索引

基于范围的 for 循环 无法 直接获取当前元素的数组索引。
由于基于范围的 for 循环始终向前迭代且不会跳过元素,因此您可以随时声明(并递增)自己的计数器。但是,如果您打算这样做,则应考虑使用普通的 for 循环是否比基于范围的 for 循环更好。

反向范围 for 循环 C++20

从 C++20 开始,您可以使用 Ranges 库的 std::views::reverse 功能来创建可遍历元素的反向视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <ranges> // C++20
#include <string_view>
#include <vector>

int main()
{
std::vector<std::string_view> words{ "Alex", "Bobby", "Chad", "Dave" }; // sorted in alphabetical order

for (const auto& word : std::views::reverse(words)) // create a reverse view
std::cout << word << ' ';

std::cout << '\n';

return 0;
}

16.9 — 使用枚举器进行数组索引和长度测量

使用非作用域枚举器进行索引

由于无作用域枚举会隐式转换为 std::size_t ,这意味着我们可以使用无作用域枚举作为数组索引来帮助记录索引的含义。

使用 非constexpr 非作用域枚举进行索引

略。

使用计数枚举器

我们在枚举器列表末尾定义了一个名为 max_students 的额外枚举器。如果之前的所有枚举器都使用默认值(建议这样做),则此枚举器的默认值将与前面枚举器的数量相匹配。在上面的示例中, max_students 的值为 5 ,因为之前定义了 5 个枚举器。我们将其非正式地称为计数枚举器 ,因为它的值表示先前定义的枚举器的数量。

使用 assert 来确保非条件数组的长度与计数枚举器的长度匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <cassert>
#include <iostream>
#include <vector>

enum StudentNames
{
kenny, // 0
kyle, // 1
stan, // 2
butters, // 3
cartman, // 4
max_students // 5
};

int main()
{
std::vector testScores { 78, 94, 66, 77, 14 };

// Ensure the number of test scores is the same as the number of students
assert(std::size(testScores) == max_students);

return 0;
}

16.10 — std::vector 调整大小和容量

固定大小数组 vs 动态数组

这类数组被称为固定大小数组固定长度数组 。std::array 和 C-style arrays 都是固定大小数组类型。
另一方面, std::vector 是一个动态数组。 动态数组 (也称为可调整大小数组 )是指在实例化之后可以更改其大小的数组。这种可调整大小的特性正是 std::vector 特殊之处。

1
std::vector<int> scores(length);

运行时调整 std::vector

std::vector 实例化后可以通过调用 resize() 成员函数并传入新的所需长度来调整其大小。

std::vector 的长度与容量

在 std::vector 的上下文中, 容量指的是 std::vector 已分配存储空间的元素数量,而长度指的是当前正在使用的元素数量。

当 std::vector 改变其管理的存储空间大小时,这个过程称为重新分配 。
由于重新分配内存通常需要复制数组中的每个元素,因此重新分配内存是一个开销很大的过程。

16.11 — std::vector 和栈的行为

调整 vector 大小与堆栈行为不兼容。
reserve() 成员函数会改变容量(但不会改变长度)。

16.12 — std::vector<bool>

优先 constexpr std::bitset 、 std::vector<char> 或第三方动态位集,而不是 std::vector<bool> 。

Quiz

没耐心做。。。以后有做不完的项目。。。以后想回顾的话再回来做吧。

  • 标题: Cpp学习20 第 16 章 动态数组:std::vector
  • 作者: 铁名_IronName
  • 创建于 : 2026-08-12 15:02:19
  • 更新于 : 2026-08-13 17:04:50
  • 链接: https://blog.ironname.top/2026/Cpp/Cpp学习20/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
Cpp学习20 第 16 章 动态数组:std::vector