迭代器适配器:反向迭代器、插入迭代器、流迭代器

迭代器这东西,说白了就是STL里的“万能指针”。但有时候,光有普通迭代器还不够用。比如你想从后往前遍历,或者想把数据直接塞进容器,又或者想把流当容器来操作——这时候就需要迭代器适配器登场了。

我个人习惯把迭代器适配器分成三类:反向迭代器插入迭代器流迭代器。它们不改变迭代器的本质,但改变了迭代器的行为方式。嗯,就像给螺丝刀换了个头,还是那个手柄,但能干不同的活。

迭代器适配器三大分类 反向迭代器 reverse_iterator • rbegin() / rend() • 底层 base() 方法 • 遍历方向相反 • 适用于所有双向容器 • 经典场景:逆序输出 插入迭代器 insert_iterator • back_inserter • front_inserter • inserter(容器, 位置) • 自动扩容,无需resize • 配合copy()使用极佳 流迭代器 stream_iterator • istream_iterator • ostream_iterator • 把流当成迭代器用 • 文件/控制台输入输出 • 延迟读取,按需迭代 核心思想:不改变迭代器接口,只改变迭代行为

一、反向迭代器:倒着走也能很优雅

反向迭代器,顾名思义,就是让迭代器从尾向头移动。你想想看,如果让你逆序打印一个vector,你会怎么写?用下标从size()-1循环到0?嗯,也不是不行,但不够STL。

反向迭代器用起来很简单:rbegin() 返回指向最后一个元素的迭代器,rend() 返回指向第一个元素之前位置的迭代器。每次 ++ 实际上是向容器头部移动。

核心要点:反向迭代器的 base() 方法可以获取对应的正向迭代器。但注意,base() 返回的位置比反向迭代器当前指向的位置后移了一位。这是设计上的有意为之,因为反向迭代器的“当前元素”和正向迭代器的“下一个元素”之间存在偏移。

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

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 反向遍历
    for (auto it = vec.rbegin(); it != vec.rend(); ++it) {
        std::cout << *it << " ";  // 输出:5 4 3 2 1
    }
    std::cout << std::endl;
    
    // base() 的偏移问题
    auto rit = vec.rbegin();      // 指向5
    auto fit = rit.base();        // 指向end(),即5之后
    std::cout << *rit << std::endl;  // 5
    // std::cout << *fit;       // 危险!fit是end()
    
    return 0;
}

我曾经踩过的坑:用反向迭代器做删除操作时,直接 erase(rit) 会编译报错。因为 erase 需要正向迭代器。正确做法是:vec.erase(std::next(rit).base()) 或者先转成正向迭代器再处理。记住,base() 返回的位置总是比反向迭代器当前位置“靠后一个”,删除时要小心这个偏移。

二、插入迭代器:赋值即插入

插入迭代器是我个人非常喜欢的一个工具。它把“赋值操作”变成了“插入操作”。你想想看,普通的迭代器赋值是覆盖已有元素,而插入迭代器赋值是往容器里添加新元素。这有什么区别?区别大了去了——你不需要提前分配空间!

STL提供了三种插入迭代器适配器:

适配器 函数 行为 适用容器
back_insert_iterator back_inserter(c) 在尾部插入(push_back) vector, deque, list, string
front_insert_iterator front_inserter(c) 在头部插入(push_front) deque, list, forward_list
insert_iterator inserter(c, pos) 在指定位置插入(insert) 所有有序容器

我最常用的场景是配合 std::copy 使用。比如从一个vector拷贝到另一个空vector:

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

int main() {
    std::vector<int> src = {10, 20, 30, 40, 50};
    std::vector<int> dst;  // 空的!没有分配空间
    
    // 直接拷贝,自动扩容
    std::copy(src.begin(), src.end(), std::back_inserter(dst));
    
    for (int x : dst) {
        std::cout << x << " ";  // 10 20 30 40 50
    }
    std::cout << std::endl;
    
    // 在中间插入
    std::vector<int> more = {100, 200};
    auto it = dst.begin() + 2;
    std::copy(more.begin(), more.end(), std::inserter(dst, it));
    // dst: 10 20 100 200 30 40 50
    
    return 0;
}

小技巧:如果你用 front_inserter 往 list 里插入,注意插入顺序是反的。因为每次都在头部插入,所以源序列的最后一个元素会变成目标序列的第一个元素。我在项目中遇到过这个“惊喜”,排查了半天才发现是顺序搞反了。

三、流迭代器:把输入输出流当容器用

流迭代器可能是最“反直觉”的迭代器适配器了。它让你把 cincout、文件流等当成一个“容器”来迭代。说白了,就是从流里读数据就像遍历一个容器,往流里写数据就像往容器里赋值。

流迭代器分两种:

  • istream_iterator:从输入流读取数据。默认构造的 istream_iterator 表示“流结束”状态(end-of-stream)。
  • ostream_iterator:向输出流写入数据。构造时需要指定分隔符(可选)。

我个人觉得流迭代器最酷的用法是配合算法,几行代码就能完成文件读写和数据处理:

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

int main() {
    // 模拟从输入流读取整数
    std::istringstream input("3 1 4 1 5 9 2 6");
    std::vector<int> nums;
    
    // 从流中读取所有整数到vector
    std::copy(
        std::istream_iterator<int>(input),
        std::istream_iterator<int>(),  // 默认构造 = 流结束
        std::back_inserter(nums)
    );
    
    // 排序
    std::sort(nums.begin(), nums.end());
    
    // 输出到控制台,用逗号分隔
    std::copy(
        nums.begin(),
        nums.end(),
        std::ostream_iterator<int>(std::cout, ", ")
    );
    // 输出:1, 1, 2, 3, 4, 5, 6, 9,
    
    return 0;
}

注意:istream_iterator 是“懒惰”的——它只在被解引用时才真正读取数据。而且一旦读取失败(比如类型不匹配或遇到文件尾),迭代器就会变成 end-of-stream 状态。我在项目中遇到过因为输入文件里混了一个非数字字符,导致整个读取流程提前终止的情况。所以,如果数据来源不可靠,建议先用字符串读进来再解析。

四、实际项目中的组合用法

这三种迭代器适配器经常组合使用。比如从文件读取数据,处理后写入另一个文件:

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

int main() {
    std::ifstream fin("input.txt");
    if (!fin) {
        std::cerr << "无法打开输入文件" << std::endl;
        return 1;
    }
    
    std::vector<double> data;
    
    // 从文件流读取所有double
    std::copy(
        std::istream_iterator<double>(fin),
        std::istream_iterator<double>(),
        std::back_inserter(data)
    );
    
    // 处理数据(比如取绝对值)
    std::transform(
        data.begin(), data.end(),
        data.begin(),
        [](double x) { return std::abs(x); }
    );
    
    // 逆序写入输出文件
    std::ofstream fout("output.txt");
    std::copy(
        data.rbegin(), data.rend(),
        std::ostream_iterator<double>(fout, "\n")
    );
    
    return 0;
}

你看,整个流程没有显式的循环,全是迭代器在干活。代码量少,可读性高,而且不容易出错——前提是你理解了每种迭代器的行为。

总结一下:反向迭代器解决“方向”问题,插入迭代器解决“空间”问题,流迭代器解决“边界”问题。三者都是对普通迭代器的功能扩展,让STL算法能应用到更广泛的场景中。我个人建议你在日常编码中多尝试用它们替代手写循环,你会发现代码变得更简洁、更“STL”。


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