迭代器适配器:反向迭代器、插入迭代器、流迭代器
迭代器这东西,说白了就是STL里的“万能指针”。但有时候,光有普通迭代器还不够用。比如你想从后往前遍历,或者想把数据直接塞进容器,又或者想把流当容器来操作——这时候就需要迭代器适配器登场了。
我个人习惯把迭代器适配器分成三类:反向迭代器、插入迭代器、流迭代器。它们不改变迭代器的本质,但改变了迭代器的行为方式。嗯,就像给螺丝刀换了个头,还是那个手柄,但能干不同的活。
一、反向迭代器:倒着走也能很优雅
反向迭代器,顾名思义,就是让迭代器从尾向头移动。你想想看,如果让你逆序打印一个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 里插入,注意插入顺序是反的。因为每次都在头部插入,所以源序列的最后一个元素会变成目标序列的第一个元素。我在项目中遇到过这个“惊喜”,排查了半天才发现是顺序搞反了。
三、流迭代器:把输入输出流当容器用
流迭代器可能是最“反直觉”的迭代器适配器了。它让你把 cin、cout、文件流等当成一个“容器”来迭代。说白了,就是从流里读数据就像遍历一个容器,往流里写数据就像往容器里赋值。
流迭代器分两种:
- 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”。