☰
C++浅析STL 迭代器 容器的使用
2026/10/20 7:10:15 网站建设 项目流程

前言


STL 的容器和迭代器是一套分工明确的接口:容器负责存储与所有权,迭代器负责以统一方式遍历和定位元素,算法则只依赖迭代器所提供的能力。理解这三者的契约,比记住每个成员函数的名字更重要。很多运行时崩溃并不是算法写错,而是迭代器在容器修改后已经失效,程序却继续使用它。


常见的误解有几个。第一,把迭代器当成指针:std::vector的迭代器在多数实现里确实是指针或指针包装,但std::list、std::map的迭代器是节点指针的抽象,std::vector<bool>的迭代器甚至返回代理对象。第二,认为所有容器的插入删除规则相同:std::vector扩容会让全部迭代器失效,而std::list插入新元素不会影响已有迭代器。第三,以为std::remove会删除元素:它只是把要保留的元素移到前面并返回新的逻辑结尾,容器大小不变,必须再调用erase。第四,在范围for循环里删除元素,这会让当前迭代器失效,属于未定义行为(undefined behavior, UB)。


本文先梳理容器分类与迭代器类别,再给出各容器在插入、删除、扩容时的迭代器失效规则,最后用完整示例演示 erase-remove、遍历中删除、std::distance/std::next以及容器选择。代码以 C++17 为基准;提到 C++20 的连续迭代器概念时会单独注明。


一、容器分类与迭代器类别


标准库容器大致分为四类。序列容器按位置存储:std::array(定长、栈上或作为成员)、std::vector(连续、可增长)、std::deque(分段连续、两端高效插入)、std::list(双向链表)、std::forward_list(单向链表)。有序关联容器std::set、std::multiset、std::map、std::multimap通常以平衡二叉搜索树实现,按比较器保持有序,查找是对数复杂度。无序关联容器std::unordered_set、std::unordered_multiset、std::unordered_map、std::unordered_multimap以哈希表实现,平均常数查找,但遍历顺序标准不保证。容器适配器std::stack、std::queue、std::priority_queue是对其他容器的封装,它们不提供迭代器,只能通过push/pop/top等接口访问。


迭代器按能力分为五类(C++20 起另有连续迭代器概念)。输入迭代器只保证单遍读取;输出迭代器只保证单遍写入;前向迭代器可以多次遍历并向前移动;双向迭代器还能后退;随机访问迭代器支持常数时间的加减、下标和比较。C++20 的std::contiguous_iterator进一步要求元素在内存中连续,std::vector、std::array、std::string的迭代器满足这一要求。




迭代器类别支持的操作典型来源



输入迭代器*it读取、++it、单遍std::istream_iterator

输出迭代器*it = v写入、++it、单遍std::ostream_iterator、std::back_insert_iterator

前向迭代器多遍遍历、只向前std::forward_list、无序容器

双向迭代器还可--itstd::list、std::set、std::map

随机访问迭代器it + n、it[n]、it1 < it2std::vector、std::deque、std::array



算法对迭代器类别有明确要求。std::sort需要随机访问迭代器,所以不能直接用于std::list;链表应调用成员函数sort()。std::find只要求输入迭代器,因此对所有容器都能用,但对std::map来说,成员find能利用有序结构做对数查找,而std::find是线性扫描,二者复杂度不同。选择算法时,先看它对迭代器的最低要求,再看容器能否提供。


const_iterator表示只读遍历,cbegin()/cend()从 C++14 起可用。普通iterator可以隐式转换为const_iterator,反向不成立。反向迭代器rbegin()/rend()把++映射为向前移动,std::reverse_iterator的base()可以取回正向迭代器,但要注意它指向的位置与解引用位置相差一个元素。


二、迭代器失效规则


迭代器失效的本质是:容器内部存储发生变化,迭代器仍指向旧位置或旧缓冲区。继续解引用、递增或比较失效迭代器都是 UB,不是「结果可能不对」这么轻。不同容器的规则差异很大,必须按容器分别记住。




容器插入操作删除操作扩容/重哈希



std::vector扩容则全部失效;未扩容时插入点及之后失效,push_back使end()失效删除点及之后失效reserve/扩容使全部失效

std::deque两端插入使所有迭代器失效但引用仍有效;中间插入使全部失效中间删除使全部失效;删除首元素只影响被删元素,删除尾元素还会使尾后迭代器失效无容量概念

std::list/std::forward_list不影响已有迭代器只使被删元素的迭代器失效不适用

std::map/std::set不影响已有迭代器只使被删元素的迭代器失效不适用

无序容器插入可能触发重哈希,重哈希使所有迭代器失效,但引用和指针仍有效只使被删元素的迭代器失效重哈希使迭代器失效

std::string类似std::vector类似std::vector扩容使全部失效



std::vector的规则最容易被低估。push_back在没有扩容时不会让已有元素的迭代器失效,但end()会失效,因为尾后位置变了。如果循环条件里缓存了end(),随后又push_back,继续用缓存的尾后迭代器就是 UB。反过来,reserve一旦改变了容量,所有迭代器、指针和引用都会失效,即使元素一个都没动。


std::deque的规则常被记成「和 vector 一样」,其实不同:在两端插入时,已有元素的引用仍然有效,但迭代器全部失效;在中间插入时,迭代器和引用都失效。无序容器则要区分「迭代器」与「引用/指针」:重哈希只影响迭代器,指向元素的引用和指针仍然有效。


遍历中删除元素时,标准库从 C++11 起让erase返回被删元素之后的迭代器,正确写法是it = c.erase(it);。对于std::vector、std::deque、std::list、std::map、std::set以及无序容器,这个返回值的语义一致。反过来,erase(it); ++it;是典型的错误:it已经失效,递增它是 UB。


三、迭代器与算法配合的完整示例


下面的例子覆盖四个高频场景:erase-remove 批量删除、遍历中条件删除、链表节点搬移、用插入迭代器写入另一个容器。


// 适用于 C++17
#include <algorithm>
#include <iostream>
#include <iterator>
#include <list>
#include <map>
#include <string>
#include <vector>

template <class C>
void print(const C& c) {
for (const auto& e : c) {
std::cout << e << ' ';
}
std::cout << '\n';
}

int main() {
std::vector<int> v{1, 2, 3, 4, 5, 6};

// remove_if 只搬移元素并返回新的逻辑结尾,容器大小不变
v.erase(std::remove_if(v.begin(), v.end(),
[](int x) { return x % 2 == 0; }),
v.end());

// 遍历中删除:必须接住 erase 的返回值
for (auto it = v.begin(); it != v.end(); ) {
if (*it == 3) {
it = v.erase(it);
} else {
++it;
}
}

std::map<std::string, int> m{{"a", 1}, {"b", 2}, {"c", 3}};
for (auto it = m.begin(); it != m.end(); ) {
if (it->second == 2) {
it = m.erase(it);
} else {
++it;
}
}

std::list<int> l{10, 20, 30};
auto pos = std::next(l.begin());
l.splice(l.end(), l, pos); // 把 20 移到末尾,其他迭代器不受影响

std::vector<int> out;
std::copy(l.begin(), l.end(), std::back_inserter(out));

print(v);
for (const auto& kv : m) {
std::cout << kv.first << '=' << kv.second << ' ';
}
std::cout << '\n';
print(out);
}

这段代码有几处值得展开。std::remove_if使用移动赋值把保留的元素向前挪,返回的迭代器指向新的逻辑结尾,它之后的元素处于有效但未指定的状态,因此必须配合erase才能缩小容器。erase-removeidiom 适用于std::vector、std::string、std::deque这类支持随机访问或至少前向遍历的序列容器;std::list虽然也能用,但直接调用成员remove_if更自然。


std::next(l.begin())返回前进一个位置后的迭代器,std::advance则是原地修改传入的迭代器且不返回值。二者对随机访问容器都是常数时间,对链表则是线性时间;在链表上频繁随机定位时,应尽量保存迭代器而不是反复std::next或std::distance。


l.splice(l.end(), l, pos)把同一链表中的单个节点摘下并接到末尾,不复制元素,也不使其他迭代器失效。std::back_inserter(out)生成一个输出迭代器,每次赋值等价于out.push_back(value),因此可以配合std::copy向空容器追加,而不需要预先resize。


范围for循环本质上是迭代器遍历的语法糖,循环期间不要修改容器大小。如果必须删除,改写成显式迭代器循环并接住erase的返回值;如果必须插入,考虑先收集要插入的位置,循环结束后统一处理。


常见坑点



  1. ❌ 在普通for循环里写if (cond) { c.erase(it); }然后继续++it。it已失效,递增它是 UB。


✅ 写it = c.erase(it);,或使用 C++20 的std::erase_if(需要 C++20 及以上,且以编译器支持为准)。



  1. ❌ 只调用std::remove(v.begin(), v.end(), x);就以为元素被删除。容器大小和末尾元素都没变。


✅ 使用 erase-remove:v.erase(std::remove(v.begin(), v.end(), x), v.end());。



  1. ❌ 对std::list调用std::sort(l.begin(), l.end())。std::sort要求随机访问迭代器,链表不满足。


✅ 调用成员函数l.sort();需要稳定排序时同样用成员版本。



  1. ❌ 在循环外缓存auto e = v.end();,循环内push_back后仍用e判断结束。


✅ 每次重新取v.end();若已知规模,先reserve避免扩容。



  1. ❌ 写for (auto x : v) { x *= 2; },修改的是元素副本,容器内容不变。


✅ 需要修改时写for (auto& x : v);只读时写for (const auto& x : v)。



  1. ❌ 在只读查找时写if (m[key] != 0)。operator[]在键不存在时会插入新元素,可能改变容器。


✅ 使用m.find(key)并判断是否等于m.end(),或使用at()(不存在时抛出异常)。



  1. ❌ 比较来自不同容器的迭代器,例如it1 < it2而二者分别指向两个vector。行为未定义。


✅ 只比较同一容器、同一有效范围内的迭代器。



  1. ❌ 用auto& b = vb[0];取std::vector<bool>元素的引用。vector<bool>是特化,operator[]返回代理对象,不能绑定到bool&。


✅ 用bool b = vb[0];读取,或用std::vector<char>代替;需要代理时按值接收。


总结




需求推荐容器迭代器能力关键注意点



连续存储、随机访问std::vector随机访问(C++20 连续)扩容使全部迭代器失效

定长、无动态分配std::array随机访问(C++20 连续)聚合体,花括号初始化

两端频繁插入删除std::deque随机访问两端插入使迭代器失效但引用有效

频繁中间插入删除std::list双向插入不影响已有迭代器,节点开销大

有序查找std::map/std::set双向用成员find,不要用operator[]查找

平均常数查找无序容器前向重哈希使迭代器失效,引用仍有效

只需栈/队列语义容器适配器无迭代器只能通过适配器接口访问



使用 STL 时,先按访问模式和修改模式选容器,再按容器记住迭代器失效规则,最后让算法与迭代器类别匹配。绝大多数「迭代器崩溃」都源于在修改容器后继续使用旧迭代器,或者在范围for中删除元素;把erase的返回值接住、把 erase-remove 写完整、不在只读查找时用operator[],就能避开最常见的几类问题。




需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询