为什么用 vector 而不是原始数组
原始数组的大小在编译时就固定下来,而且一旦传给函数,就忘了自己有多长。std::vector 解决了这两个问题:它是一个可变大小的数组,会记录自己的长度、按需增长,并自己清理内存。在现代 C++ 中,vector 是默认的容器,只有当你有特别的理由时才去用原始数组。
包含 <vector>,然后在尖括号里写上元素类型来声明一个 vector:
scores.size() 总能告诉你当前的长度:不需要另外维护一个 int n,也不需要 sizeof 技巧。{90, 75, 100, 60} 是一个花括号初始化器,vector 会自己算出它需要四个位置。
创建和初始化 vector
根据你事先知道的信息,有好几种创建方式:
注意圆括号与花括号的陷阱:vector<int> tens(5, 10) 创建的是五个 10,而 vector<int> tens{5, 10} 创建的是一个包含 5 和 10 两个元素的 vector。圆括号表示“大小和填充值”,花括号表示“就是这些元素”。
添加和删除元素
vector 的意义就在于它能增长。push_back 在末尾追加元素,pop_back 从末尾删除元素:
back() 返回最后一个元素,front() 返回第一个,比 v[v.size() - 1] 和 v[0] 更简洁。从 C++11 开始,你还可以用 emplace_back(args...) 原地构造元素,对于较重的类型可以省去一次临时复制。
初学者的一个常见错误是对空 vector 调用 front() 或 back()。这是未定义行为,而不是报错,所以一定要先用 if (!v.empty()) 做检查。
读取元素:[] 与 at()
你可以像数组一样用 [] 按下标访问 vector。但 [] 不做边界检查,越界的下标是未定义行为,可能悄悄读到垃圾值,或者稍后在一个令人困惑的地方崩溃:
vector<int> v = {1, 2, 3};
cout << v[10]; // UNDEFINED BEHAVIOR - no check, no error
想要安全时,就用 at()。它会检查下标,访问无效时抛出 std::out_of_range,这是一种你可以捕获的标准库异常,所以你得到的是一个清晰的失败,而不是数据损坏:
经验法则:在已经确认下标有效的紧凑循环里用 [],在可能混入错误输入的边界处用 at()。
遍历 vector
遍历 vector 最简洁的方式是基于范围的 for 循环。用 const auto& 获取元素可以只读而不复制,用 auto& 则可以原地修改:
如果确实需要下标(比如要比较相邻元素),就用传统的计数循环。但要注意 size() 返回的是无符号类型(size_t)。拿有符号的 int i 和它比较,可能触发编译器警告和出人意料的回绕,所以尽量使用 size_t i 或基于范围的循环:
for (size_t i = 0; i < v.size(); i++) { // size_t, not int
cout << v[i];
}
size、capacity 与 reserve
vector 维护着两个数字:size()(当前存了多少个元素)和 capacity()(在必须增长之前能存多少个)。当一次 push_back 超出容量时,vector 会分配一块更大的内存,把每个元素复制过去,再释放旧的内存块。这就是为什么反复 push_back 的均摊成本很低,但每一次重新分配本身并不是免费的:
如果你大致知道要添加多少个元素,就先调用 reserve(),省掉反复的重新分配。注意 reserve() 改变的是容量而不是大小:在你放入元素之前,vector 里仍然是零个元素。
这种重新分配也是 vector 最棘手 bug 的根源。因为增长会移动存储,你保存下来的任何指向 vector 内部的指针、引用或迭代器,在一次触发重新分配的 push_back 之后都会变成悬空的:
vector<int> v = {1, 2, 3};
int& first = v[0]; // reference into the vector
v.push_back(4); // may reallocate...
cout << first; // DANGLING - may point at freed memory
迭代器也是一样:用保存下来的迭代器遍历时,不要 push_back 或 erase。如果必须在循环中删除元素,请使用 erase 的返回值,或者使用配合 std::remove 的 erase-remove 惯用法。
下一步:map
当你按位置查找时(第 0 个元素、第 1 个元素,依此类推),vector 是完美的选择。但很多时候你想按键来查找:用户名、商品 ID、一个单词。这正是 std::map 的用途。接下来我们会讲解 C++ 的键值容器 map,包括如何插入、查找和遍历条目,以及几乎每个人都会踩到的“[] 会创建默认值”的陷阱。
常见问题
C++ 中的 vector 是什么?
std::vector 是 C++ 标准库中的动态(可变大小)数组。和原始数组不同,它知道自己的大小,用 push_back 添加元素时会自动增长,还会替你释放内存。包含 <vector>,写 vector<int> v; 就能创建一个。
C++ vector 的 [] 和 at() 有什么区别?
v[i] 不做边界检查:越界的下标是未定义行为(崩溃或悄悄破坏数据)。v.at(i) 会检查下标,无效时抛出 std::out_of_range。在已经验证过下标的热点循环里用 [],想要安全、便于调试的失败方式时用 at()。
push_back 会让指向 C++ vector 元素的指针和引用失效吗?
会,有这个可能。当 vector 的容量用完时,push_back 会把存储重新分配到一块新内存,这会让指向旧元素的所有指针、引用和迭代器失效。不要在 push_back 前后一直持有某个元素的引用;如果可以,提前调用 reserve(),避免意外的重新分配。