迭代器适配器:让迭代器变得更强大

迭代器适配器,说白了就是给迭代器加了一层「外挂」。它们不直接操作容器,而是包装另一个迭代器,改变其行为。我个人觉得,这是 STL 里最被低估的工具之一。你想想看,很多时候我们写循环、手动 push_back,其实都可以用这些适配器一行搞定。

插入迭代器:自动插入,省心省力

插入迭代器的作用,就是把「赋值操作」变成「插入操作」。你往迭代器里写数据,它自动帮你插到容器里。我刚开始用的时候,觉得这玩意儿有点多余,直到有一次重构代码,发现几十个 push_back 全被替换成了 copy + back_inserter,代码量直接砍半。

back_inserter:尾部插入

最常用的插入迭代器。它调用容器的 push_back() 方法。注意,容器必须支持 push_back,比如 vectordequelist 都可以,但 forward_list 不行。

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

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

    // 用 back_inserter 自动尾部插入
    std::copy(src.begin(), src.end(), std::back_inserter(dst));

    for (int v : dst) std::cout << v << " ";
    // 输出:1 2 3 4 5
    return 0;
}

小技巧:如果你提前知道要插入多少元素,先用 reserve() 分配好空间,再配合 back_inserter,性能会好很多。我曾经在项目里没注意这个,结果插入 10 万条数据时反复扩容,慢了将近 3 倍。

front_inserter:头部插入

这个适配器调用容器的 push_front()。只有 dequelistforward_list 支持。注意,vector 没有 push_front,所以不能用。

#include <iostream>
#include <deque>
#include <iterator>
#include <algorithm>

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

    // 把 src 插入到 dq 头部
    std::copy(src.begin(), src.end(), std::front_inserter(dq));

    for (int v : dq) std::cout << v << " ";
    // 输出:2 1 3 4 5
    return 0;
}

注意:front_inserter 时,插入顺序是反的。因为每次插入都在头部,所以第一个元素会被挤到最后。上面例子中,src 是 {1, 2},但插入后 dq 头部变成了 2、1。如果你想要保持原顺序,要么先反转 src,要么改用 back_inserter

inserter:任意位置插入

这个最灵活。它需要两个参数:容器和一个迭代器,表示插入位置。它调用容器的 insert() 方法,每次插入后,迭代器会自动前进到下一个位置。

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

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

    // 在 vec[2] 位置(即 5 之前)插入 src
    auto it = vec.begin() + 2;
    std::copy(src.begin(), src.end(), std::inserter(vec, it));

    for (int v : vec) std::cout << v << " ";
    // 输出:1 2 3 4 5 6
    return 0;
}

核心区别:

  • back_inserter:尾部插入,需要 push_back
  • front_inserter:头部插入,需要 push_front,顺序反转
  • inserter:任意位置插入,需要 insert,顺序保持

流迭代器:把输入输出当成容器

流迭代器让你像操作容器一样操作 cincout、文件流等。说白了,就是把数据流包装成迭代器接口。我最早用这个是在写一个日志解析工具,直接从 cin 读数据,用 copy 写到 vector 里,一行代码搞定读取。

istream_iterator:从输入流读取

它从输入流中读取指定类型的数据。默认构造的 istream_iterator 表示「流结束」,通常用作 copy 的结束迭代器。

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

int main() {
    std::vector<int> nums;

    // 从标准输入读取整数,直到遇到非数字或 EOF
    std::copy(
        std::istream_iterator<int>(std::cin),
        std::istream_iterator<int>(),
        std::back_inserter(nums)
    );

    for (int v : nums) std::cout << v << " ";
    return 0;
}

输入:1 2 3 4 5 ^Z(Windows 下 Ctrl+Z,Linux 下 Ctrl+D)
输出:1 2 3 4 5

避坑指南:我曾经在项目里用 istream_iterator 读文件,结果发现它默认跳过空白字符。如果你需要读取包含空格的字符串,比如一行文本,那就不能用它了。这时候得用 std::getline 配合 istringstream

ostream_iterator:向输出流写入

它把数据写入输出流。你可以指定分隔符,比如空格、逗号等。

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

int main() {
    std::vector<int> nums = {10, 20, 30, 40, 50};

    // 输出到标准输出,用逗号+空格分隔
    std::copy(
        nums.begin(), nums.end(),
        std::ostream_iterator<int>(std::cout, ", ")
    );
    // 输出:10, 20, 30, 40, 50,
    return 0;
}

注意最后一个元素后面也会跟分隔符。如果你不想这样,可以自己写个循环,或者用 fmt 库(C++20 之后有 std::format)。

移动迭代器:把拷贝变成移动

移动迭代器是 C++11 引入的。它把解引用操作返回左值引用变成右值引用,从而触发移动语义。说白了,就是让 copy 变成 move

我印象最深的一次,是在处理一个包含大量 std::stringvector 时。原来的代码用 std::copy 把数据从一个容器拷贝到另一个,结果性能很差。换成 std::move_iterator 后,字符串的底层内存直接被「偷」走了,拷贝开销几乎为零。

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

int main() {
    std::vector<std::string> src = {"hello", "world", "from", "move_iterator"};
    std::vector<std::string> dst;

    // 移动而非拷贝
    std::copy(
        std::make_move_iterator(src.begin()),
        std::make_move_iterator(src.end()),
        std::back_inserter(dst)
    );

    std::cout << "src size: " << src.size() << "\n";  // 4
    std::cout << "src[0]: '" << src[0] << "'\n";      // 空字符串(已被移动)
    std::cout << "dst[0]: '" << dst[0] << "'\n";      // "hello"

    return 0;
}

重要提醒:移动迭代器会「掏空」源对象。移动之后,源对象处于有效但未指定的状态。你不能再依赖它的值。所以,只有当你确定源对象不再需要时,才用移动迭代器。我曾经在代码审查时看到有人对同一个容器既用 move_iterator 又后续读取,结果数据全丢了,排查了半天。

知识结构图

下面这张图帮你理清这几种迭代器适配器的关系和使用场景:

迭代器适配器分类与使用场景 迭代器适配器 插入迭代器 流迭代器 移动迭代器 back_inserter front_inserter inserter istream_iterator ostream_iterator make_move_iterator 使用建议 • 插入迭代器:适合用算法向容器中插入数据,避免手写循环 • 流迭代器:适合从输入流读取或向输出流写入,配合 copy 一行搞定 • 移动迭代器:适合转移大型对象所有权,减少拷贝开销

总结

迭代器适配器是 STL 算法和容器之间的「胶水」。它们让算法可以灵活地输出到不同目标:容器、流、或者移动语义。我个人觉得,掌握它们之后,写出来的代码会更简洁、更优雅,也更容易维护。

嗯,最后提醒一句:front_inserter 的顺序问题、move_iterator 的源对象失效问题,都是容易踩的坑。写代码时多留个心眼,就能避免很多不必要的调试时间。


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