7、vector容器(下):访问、遍历、预留空间与迭代器失效
好,咱们接着聊 vector。上一章我们把 vector 的构造、赋值、增删操作都过了一遍。这一章,我打算把剩下的几个关键点一口气讲完——访问与遍历、预留空间、收缩内存,还有那个让无数新手头疼的迭代器失效问题。
这几个知识点,说白了就是「怎么用好 vector」和「怎么避免踩坑」。我在项目里见过太多人因为不懂 reserve 和 shrink_to_fit,把性能搞得一塌糊涂;也见过有人被迭代器失效折磨到怀疑人生。嗯,咱们一个一个来。
7.1 vector 的访问与遍历
vector 的访问方式,其实就那么几种。我个人的习惯是:能用下标就别用 at(),除非你特别在意边界检查。
7.1.1 下标访问(operator[])
这是最直接的方式。不检查边界,速度快。你心里得有数——越界了就是未定义行为,程序可能直接崩。
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {10, 20, 30, 40, 50};
// 下标访问
std::cout << vec[0] << std::endl; // 10
std::cout << vec[2] << std::endl; // 30
// 越界!危险!
// std::cout << vec[10] << std::endl; // 未定义行为
return 0;
}
7.1.2 at() 成员函数
at() 会做边界检查。越界时抛出 std::out_of_range 异常。安全,但有一点点性能开销。
try {
std::cout << vec.at(10) << std::endl;
} catch (const std::out_of_range& e) {
std::cerr << "越界了:" << e.what() << std::endl;
}
我个人在调试阶段喜欢用 at(),上线前再改成 operator[]。你想想看,调试时能帮你快速定位越界问题,多省心。
7.1.3 front() 和 back()
这两个函数分别返回第一个和最后一个元素的引用。注意:空 vector 上调用它们,行为未定义。
std::vector<int> vec = {1, 2, 3, 4, 5};
std::cout << vec.front() << std::endl; // 1
std::cout << vec.back() << std::endl; // 5
7.1.4 遍历方式
遍历 vector 的方式有好几种。我列个表,你一看就明白。
| 遍历方式 | 代码示例 | 特点 |
|---|---|---|
| 下标遍历 | for (size_t i = 0; i < vec.size(); ++i) |
最传统,可修改元素 |
| 范围 for | for (auto& v : vec) |
简洁,C++11 起推荐 |
| 迭代器遍历 | for (auto it = vec.begin(); it != vec.end(); ++it) |
通用,适用于所有容器 |
| for_each + lambda | std::for_each(vec.begin(), vec.end(), [](int v){...}) |
函数式风格,可读性强 |
// 范围 for(我最推荐的方式)
for (const auto& val : vec) {
std::cout << val << " ";
}
// 迭代器遍历
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
// for_each + lambda
std::for_each(vec.begin(), vec.end(), [](int v) {
std::cout << v << " ";
});
7.2 vector 的预留空间(reserve)
这是 vector 性能优化的核心之一。很多新手不知道,vector 在 push_back 时,如果容量不够,会重新分配内存、拷贝所有元素、释放旧内存。这个过程很昂贵。
reserve 的作用:提前分配好足够的内存,避免多次重新分配。
#include <vector>
#include <iostream>
int main() {
std::vector<int> vec;
vec.reserve(1000); // 提前分配 1000 个 int 的空间
std::cout << "size: " << vec.size() << std::endl; // 0
std::cout << "capacity: " << vec.capacity() << std::endl; // 1000
for (int i = 0; i < 1000; ++i) {
vec.push_back(i); // 不会触发重新分配
}
return 0;
}
我在项目中遇到过这样一个场景:一个网络服务要接收大量数据包,每个包解析后存入 vector。如果不 reserve,每 push_back 一次都可能触发内存重分配,性能直接崩了。加了 reserve 后,吞吐量提升了 3 倍。
7.2.1 reserve 与 resize 的区别
这两个函数经常被搞混。我画个表帮你理清。
| 函数 | 改变 size? | 改变 capacity? | 创建元素? |
|---|---|---|---|
| reserve(n) | 否 | 是(只增不减) | 否 |
| resize(n) | 是 | 可能 | 是(默认构造或拷贝) |
std::vector<int> vec;
vec.reserve(10); // capacity = 10, size = 0
vec.resize(5); // capacity >= 10, size = 5, 5 个元素被默认初始化为 0
7.3 vector 的收缩(shrink_to_fit)
有时候你往 vector 里塞了很多数据,处理完后删掉了一大半。但 capacity 还保留着原来的大小,白白浪费内存。这时候就该 shrink_to_fit 上场了。
std::vector<int> vec;
for (int i = 0; i < 1000; ++i) {
vec.push_back(i);
}
std::cout << "capacity: " << vec.capacity() << std::endl; // 可能是 1024
vec.clear(); // 清空元素,但 capacity 不变
std::cout << "capacity after clear: " << vec.capacity() << std::endl; // 还是 1024
vec.shrink_to_fit(); // 请求收缩到合适大小
std::cout << "capacity after shrink: " << vec.capacity() << std::endl; // 可能是 0
我曾经在一个嵌入式项目里用过 shrink_to_fit。设备内存只有 64MB,一个临时 vector 在处理完数据后还占着 30MB 不释放。加上 shrink_to_fit 后,内存占用降到了 2MB,设备再也不 OOM 了。
7.4 vector 的迭代器失效
这是 vector 里最容易踩的坑,没有之一。我刚开始用 C++ 时,被这个问题折磨了整整两天。后来我总结了一句话:任何可能改变 vector 容量的操作,都会使所有迭代器、指针、引用失效。
7.4.1 哪些操作会导致迭代器失效?
- push_back / emplace_back:如果导致重新分配,所有迭代器失效。否则只有 end() 失效。
- insert:如果导致重新分配,所有迭代器失效。否则插入位置之后的所有迭代器失效。
- erase:删除位置之后的所有迭代器失效(包括 end())。
- reserve / resize:如果改变了 capacity,所有迭代器失效。
- clear:所有迭代器失效。
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin() + 2; // 指向 3
vec.push_back(6); // 如果重新分配,it 失效!
// 此时使用 *it 是未定义行为
// 正确的做法:重新获取迭代器
it = vec.begin() + 2;
7.4.2 删除元素时的迭代器失效
这是最常见的场景。很多人这样写:
// 错误示例
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it % 2 == 0) {
vec.erase(it); // 删除后 it 失效,再 ++it 是未定义行为!
}
}
正确的做法是利用 erase 的返回值:
// 正确示例
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it % 2 == 0) {
it = vec.erase(it); // erase 返回下一个有效迭代器
} else {
++it;
}
}
// 结果:{1, 3, 5}
7.4.3 避免迭代器失效的最佳实践
- 如果需要在遍历时频繁删除,考虑使用 std::list 或 std::forward_list。
- 如果必须用 vector,先 reserve 足够的容量,避免插入时重新分配。
- 使用下标索引代替迭代器(如果不需要删除操作)。
- 使用 erase-remove 惯用法:
vec.erase(std::remove_if(vec.begin(), vec.end(), pred), vec.end());
// erase-remove 惯用法(推荐)
std::vector<int> vec = {1, 2, 3, 4, 5};
vec.erase(std::remove_if(vec.begin(), vec.end(),
[](int v) { return v % 2 == 0; }),
vec.end());
// 结果:{1, 3, 5}
这个惯用法的好处是:所有操作都在一个表达式中完成,不会出现迭代器失效的问题。我个人在项目里几乎都用这种方式来删除元素。
7.5 知识体系总览
下面这张图把本章的核心知识点串起来了。你可以把它当作一个快速参考。
这张图把本章的四个核心主题串在了一起。访问与遍历是日常使用最多的;reserve 和 shrink_to_fit 是性能优化的利器;迭代器失效则是你必须时刻警惕的陷阱。把这四点吃透,vector 你就算真正掌握了。
公众号:蓝海资料掘金营,微信deep3321