迭代器到底是什么
每个标准容器,vector、string、map、set、list,内部存储元素的方式都不同。vector 是一块连续内存,map 是一棵平衡树,list 是一串链表节点。但你可以用同样的方式遍历它们。让这一切成为可能的就是迭代器:一个“指向”某个元素、并且知道如何走到下一个元素的小对象。
可以把迭代器看作一种泛化的指针。用 begin() 获取一个迭代器,用 * 读取它指向的元素,用 ++ 让它前进。这几部分是这样组合在一起的:
v.begin() 返回指向第一个元素的迭代器;*it 得到这个元素;++it 移动到下一个元素。解引用、前进、比较,这三个操作就是全部的思维模型。
begin()、end() 与半开区间
另一半是 end()。关键在于,end() 不指向最后一个元素,而是指向最后一个元素之后的那个位置。这是有意设计的“半开”区间 [begin, end):包含 begin,而 end 是停止信号。
这种设计让标准的循环写法非常简洁:一直遍历到迭代器等于 end() 为止。
注意写的是 it != v.end(),而不是 it < v.end()。大多数容器的迭代器(比如 map 或 list 的)不支持 <,只支持 == 和 !=,所以 != 是通用的写法。另外,auto 让你不必手写 vector<int>::iterator,编译器会自动推导。
空容器的情况自然就处理好了:容器为空时 begin() == end(),所以循环体一次也不会执行,不需要做特殊处理。
永远不要解引用 end()
最常见的迭代器 bug 就是解引用 end()。因为它指向最后一个元素之后的位置,*v.end() 读取的是不属于你的内存,这是未定义行为,结果可能是崩溃,也可能是悄无声息的垃圾值,而不会有友好的错误提示:
vector<int> v = {1, 2, 3};
cout << *v.end(); // UNDEFINED BEHAVIOR - end() is not an element
查找函数也有同样的陷阱。std::find 找不到值时会返回 end(),所以解引用之前必须先检查:
在解引用返回的迭代器之前,一定要先把它和 end() 比较。忘记写这个 if,是初学者 STL 代码崩溃最常见的原因之一。
const、cbegin 与反向迭代器
容器会根据你的需要提供不同种类的迭代器:
begin()/end():普通的读写迭代器(可以写*it = ...)。cbegin()/cend():const_iterator,可以通过它读取元素,但不能修改。rbegin()/rend():从后往前遍历的反向迭代器,++实际上是向后移动。
想要倒序遍历,又不想做繁琐的下标运算,反向迭代器是最简洁的办法:
使用反向迭代器时,前进仍然写 ++it,“向后”的方向由迭代器在内部处理。当一个循环只需要读取时,使用 cbegin()/cend()(或容器的 const 引用),这样编译器会阻止你意外写入。
map 迭代器得到的是 pair
并不是每个迭代器都只是指针的简单包装。std::map 的迭代器要遍历一棵树,解引用它会得到一个由键和值组成的 std::pair,通过 ->first 和 ->second 访问(和指针一样,迭代器也支持 ->):
基于范围的 for 循环就是直接建立在 begin()/end() 之上的,所以对于普通的正向遍历,你通常会改用它。当你需要反向遍历、需要知道元素的位置,或者要把一个区间传给算法时,显式的迭代器才真正派上用场。
最大的陷阱:迭代器失效
这是每个人迟早都会踩到的坑。当你改变容器的结构时,已有的迭代器可能会失效:它们指向的内存已被释放或移动。使用失效的迭代器是未定义行为。
对于 vector,push_back 可能为了扩容而重新分配整个缓冲区,使所有现存的迭代器失效。在循环中删除元素更是臭名昭著,这是一个经典的崩溃案例:
vector<int> v = {1, 2, 3, 4};
for (auto it = v.begin(); it != v.end(); ++it) {
if (*it % 2 == 0)
v.erase(it); // BUG - erase invalidates it, then ++it is UB
}
解决办法是:erase 会返回一个指向被删除元素下一个元素的有效迭代器。只有在没有删除时才前进:
注意,for 的头部没有 ++it,由循环体决定是否前进。(在实际代码中,erase-remove 惯用法或 C++20 的 std::erase_if 只需一行就能做到。)要记住的规则是:任何添加或删除元素的操作都可能使迭代器失效,所以不要在这类操作前后一直使用同一个旧迭代器。
下一步:算法
既然你已经能用一对 begin/end 来描述一个区间,整个 STL 算法库就向你敞开了。sort、find、count、accumulate 这些函数并不关心你用的是哪种容器,它们作用于迭代器区间,所以同样的调用既能用于 vector,也能用于数组或其中的一段。接下来我们会让这些迭代器派上用场,让标准库替你完成循环。
常见问题
C++ 中的迭代器是什么?
迭代器是一个指向容器内某个元素、并且知道如何移动到下一个元素的对象。用 container.begin() 获取指向第一个元素的迭代器,用 container.end() 获取指向末尾之后位置的标记。用 *it 解引用来读写元素,用 ++it 前进。迭代器是一套通用接口,正是它让 STL 算法能够作用于任何容器。
C++ 中迭代器和指针有什么区别?
对于 vector 或数组,迭代器的行为几乎和指针一模一样:用 * 解引用,用 ++ 前进,用 ==/!= 比较。但迭代器是一个概念,不一定是原始指针:map 或 list 的迭代器要遍历树或链表节点,所以它是一个重载了 * 和 ++ 的类类型。指针是迭代器的一种,迭代器把这个思想推广到了所有容器。
C++ 中什么会导致迭代器失效?
修改容器的结构,可能让已有的迭代器指向已被释放或移动的内存。对于 vector,push_back 可能重新分配内存,使所有迭代器失效;erase 会使被删除元素及其之后的迭代器失效。使用失效的迭代器是未定义行为。为了安全,请使用 erase 返回的迭代器,或者预先预留容量。