迭代器基础:从输入到随机访问,再到失效与反向

迭代器,说白了就是 STL 的「万能胶水」。它把容器和算法粘在一起。我刚开始学 STL 时,总觉得迭代器很抽象——不就是个指针吗?后来踩过坑才明白,迭代器的分类体系,才是 STL 设计的精髓。

迭代器的五种分类

STL 把迭代器分成五类,每一类能做的事情不一样。你可以把它们想象成不同级别的「通行证」。

类别 能力 典型容器
输入迭代器 只读,单向,一次遍历 istream_iterator
输出迭代器 只写,单向,一次遍历 ostream_iterator
前向迭代器 读写,单向,可多次遍历 forward_list
双向迭代器 读写,双向 list, set, map
随机访问迭代器 读写,双向,支持跳跃 vector, deque, array

我个人习惯把随机访问迭代器叫做「全能选手」。它支持 it + nit - nit[n],甚至 it1 - it2 算距离。而输入迭代器只能 ++it*it,连 --it 都不行。

核心原则:算法对迭代器类型的要求,只高不低。比如 sort() 要求随机访问迭代器,你拿 list 的双向迭代器去用,编译器直接报错。

输入/输出迭代器:最朴素的读写

输入迭代器,你想想看,就像从键盘读数据——只能往前读,不能回头。输出迭代器则像往屏幕写数据——写出去就没了。

#include <iostream>
#include <iterator>
#include <vector>
#include <algorithm>

int main() {
    // 输入迭代器:从标准输入读整数
    std::istream_iterator<int> in_iter(std::cin);
    std::istream_iterator<int> eof;  // 结束标记

    // 输出迭代器:写到标准输出,用空格分隔
    std::ostream_iterator<int> out_iter(std::cout, " ");

    // 把输入读到 vector 里
    std::vector<int> vec(in_iter, eof);

    // 排序后输出
    std::sort(vec.begin(), vec.end());
    std::copy(vec.begin(), vec.end(), out_iter);

    return 0;
}

我在项目中遇到过用 istream_iterator 读配置文件的情况。当时觉得挺酷,一行代码就把文件里的数字全读进来了。但要注意——输入迭代器只能遍历一次,你不能把 in_iter 保存起来后面再用。

前向迭代器与双向迭代器

前向迭代器比输入/输出迭代器强在哪?它可以多次遍历同一个序列。你想想看,输入迭代器读完一个元素就「废」了,前向迭代器可以反复读。

双向迭代器就更灵活了,能 ++ 也能 --。我早期写代码时,用 std::list 存了一堆任务,需要在中间插入和删除。双向迭代器让我能从容地前后移动。

小技巧:如果你只需要单向遍历,用前向迭代器就够了。别为了「万一以后需要反向」而选择双向迭代器——那通常意味着你选错了容器。

随机访问迭代器:性能之王

随机访问迭代器支持 it + 5 这种跳跃操作。为什么 vector 的 sort() 比 list 快那么多?就是因为随机访问迭代器能直接跳到中间位置,而双向迭代器只能一步一步挪。

#include <vector>
#include <algorithm>

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

    // 随机访问迭代器支持指针算术
    auto mid = vec.begin() + vec.size() / 2;
    
    // 可以比较大小
    if (vec.begin() < mid) {
        // 前半部分排序
        std::sort(vec.begin(), mid);
    }

    // 可以计算距离
    auto dist = std::distance(vec.begin(), vec.end());
    // 等价于 vec.end() - vec.begin()

    return 0;
}

嗯,这里要注意:std::distance() 对随机访问迭代器是 O(1) 的,但对双向迭代器是 O(n) 的。我见过有人对 list 用 distance(),结果性能崩了。

迭代器失效问题:避坑指南

迭代器失效,是 C++ 新手最容易踩的坑之一。说白了就是:你拿着一个迭代器,指向容器里的某个元素,然后你修改了容器,这个迭代器就「废」了。

操作 vector list deque
插入 所有迭代器失效 仅当前迭代器有效 所有迭代器失效
删除 删除点之后全失效 仅被删元素失效 所有迭代器失效
push_back 可能全部失效(扩容) 不影响 不影响

我曾经在项目中犯过一个经典错误:在遍历 vector 时删除元素,结果迭代器失效导致崩溃。正确的做法是用 erase() 返回的迭代器继续遍历。

// 错误示范:迭代器失效
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 失效了
    }
}

// 正确做法
for (auto it = vec.begin(); it != vec.end(); ) {
    if (*it % 2 == 0) {
        it = vec.erase(it);  // erase 返回下一个有效迭代器
    } else {
        ++it;
    }
}

反向迭代器:从尾到头

反向迭代器,说白了就是把正向迭代器「倒过来用」。它用 rbegin()rend() 获取,内部其实存的是正向迭代器。

我记得有一次要按时间倒序显示日志。用反向迭代器一行代码搞定:

#include <vector>
#include <iostream>

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

    // 反向遍历
    for (auto rit = logs.rbegin(); rit != logs.rend(); ++rit) {
        std::cout << *rit << " ";  // 输出:5 4 3 2 1
    }

    // 注意:反向迭代器的 base() 返回对应的正向迭代器
    auto rit = logs.rbegin();
    auto it = rit.base();  // it 指向 logs[4] 的下一个位置
    // 也就是说,rit 和 it 指向的元素是相邻的

    return 0;
}

小细节:rbegin() 对应 end()rend() 对应 begin()。这个对应关系有点绕,我建议你画个图理解一下。简单记:反向迭代器的 base() 总是指向正向的下一个位置。

知识体系总览

下面这张图把迭代器的分类、能力、典型容器串起来了。你可以把它当作速查表。

迭代器分类体系 输入迭代器 输出迭代器 前向迭代器 双向迭代器 随机访问迭代器 能力递增 → 只读/只写 → 读写 → 跳跃 vector, deque, array list, set, map forward_list istream_iterator ostream_iterator

迭代器这东西,刚开始觉得抽象,用多了就发现它其实很自然。记住一条:能用低级别迭代器解决的问题,就别用高级别的。这既是性能考虑,也是接口设计的原则。

总结一下:迭代器是 STL 的骨架。理解分类体系,你就理解了为什么有些算法不能用在某些容器上。迭代器失效是运行时的大坑,写代码时多留个心眼。反向迭代器是个小工具,用好了能让代码更简洁。


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