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 << " ";
});
我的建议:日常开发用范围 for,简洁且不易出错。需要操作迭代器时(比如删除元素),再用显式迭代器。

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 倍。

核心要点:reserve 只改变 capacity,不改变 size。它不创建元素,只是预留内存。

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 是一个非强制性请求。标准库实现可以选择忽略它。不过主流编译器(GCC、Clang、MSVC)通常都会执行。

我曾经在一个嵌入式项目里用过 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}
黄金法则:在 vector 的迭代器循环中,如果调用了 erase 或 insert,一定要使用返回值更新迭代器。不要假设迭代器仍然有效。

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 知识体系总览

下面这张图把本章的核心知识点串起来了。你可以把它当作一个快速参考。

vector 核心操作 访问与遍历 下标 operator[] at() 边界检查 front() / back() 范围 for / 迭代器 预留空间 reserve 只增 capacity 不创建元素 避免多次重分配 与 resize 区别 收缩 shrink_to_fit 释放多余内存 非强制性请求 clear 后使用 迭代器失效(核心坑点) push_back / insert / erase 导致失效 使用 erase 返回值更新迭代器 推荐 erase-remove 惯用法

这张图把本章的四个核心主题串在了一起。访问与遍历是日常使用最多的;reserve 和 shrink_to_fit 是性能优化的利器;迭代器失效则是你必须时刻警惕的陷阱。把这四点吃透,vector 你就算真正掌握了。


公众号:蓝海资料掘金营,微信deep3321