迭代器深入:迭代器分类与适配器实战
迭代器这东西,说白了就是STL的"万能胶"。它把容器和算法粘在一起,让一套算法能适配各种数据结构。我刚开始学STL时,总觉得迭代器不就是个指针吗?后来踩了不少坑才明白——迭代器远比指针复杂,而且分类体系才是它的精髓。
核心认知:迭代器分类决定了你能用什么算法。选错迭代器类型,轻则编译报错,重则运行时崩溃。
7.1 迭代器分类体系
STL定义了五种迭代器,按能力从弱到强排列。我习惯把它们分成三个梯队:
7.2 各分类详解
输入迭代器(Input Iterator)
只能读,不能写。而且读一次就没了——你想想看,从键盘读数据,读完了还能再读一遍吗?
// 典型例子:istream_iterator
std::istream_iterator<int> it(std::cin);
std::istream_iterator<int> end; // 哨兵迭代器
// 只能单向遍历,不能回退
int val = *it; // 读取
++it; // 前进
// --it; // 错误!不支持回退
实战经验:我曾在日志解析模块里用istream_iterator配合copy算法,一行代码就把文件里的数字全读进vector。但要注意——输入迭代器是"一次性"的,别想着反复遍历。
输出迭代器(Output Iterator)
只能写,不能读。说白了就是个"只管往外倒"的接口。
// 典型例子:ostream_iterator
std::ostream_iterator<int> it(std::cout, ", ");
*it = 42; // 写入
++it; // 前进
*it = 100; // 再写入
前向迭代器(Forward Iterator)
既能读又能写,还能反复遍历同一段数据。forward_list的迭代器就是典型。
双向迭代器(Bidirectional Iterator)
在前向迭代器基础上,多了个--操作。list、set、map的迭代器都属于这一类。
随机访问迭代器(Random Access Iterator)
这是"满血版"迭代器。vector、deque、array的迭代器都支持。你可以像操作指针一样:
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin();
it += 3; // 跳3步
int val = it[1]; // 下标访问
bool cmp = it < vec.end(); // 比较大小
避坑指南:我曾经在代码里用std::sort对std::list排序,结果编译报错。为什么?因为sort要求随机访问迭代器,而list只提供双向迭代器。后来改用list::sort()才搞定。记住:算法对迭代器类型有硬性要求。
7.3 迭代器适配器
迭代器适配器,就是在已有迭代器上"套一层",改变它的行为。我常用的有三个:
| 适配器 | 作用 | 典型场景 |
|---|---|---|
| reverse_iterator | 反向遍历 | 从尾到头处理数据 |
| back_insert_iterator | 自动调用push_back | 向容器尾部插入数据 |
| front_insert_iterator | 自动调用push_front | 向容器头部插入数据 |
reverse_iterator 实战
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
}
// 注意:rbegin() 指向最后一个元素
// rend() 指向第一个元素之前
back_insert_iterator 实战
std::vector<int> src = {1, 2, 3};
std::vector<int> dst;
// 不用back_inserter的话,你得手动resize
// 用了它,自动push_back
std::copy(src.begin(), src.end(), std::back_inserter(dst));
// dst 现在包含 1, 2, 3
个人习惯:我写算法时,只要目标容器可能为空,一律用back_inserter。它省去了手动管理内存的麻烦,代码也更安全。
7.4 自定义迭代器实现
有时候STL提供的迭代器不够用,你得自己写。比如封装一个自定义容器,或者给现有容器加特殊行为。
下面我实现一个简单的"步进迭代器",每次跳2步:
template<typename Iterator>
class StepIterator {
public:
using iterator_category = std::forward_iterator_tag;
using value_type = typename std::iterator_traits<Iterator>::value_type;
using difference_type = typename std::iterator_traits<Iterator>::difference_type;
using pointer = typename std::iterator_traits<Iterator>::pointer;
using reference = typename std::iterator_traits<Iterator>::reference;
StepIterator(Iterator it) : current_(it) {}
reference operator*() const { return *current_; }
pointer operator->() const { return &(*current_); }
// 每次跳2步
StepIterator& operator++() {
++current_;
++current_; // 跳2步
return *this;
}
StepIterator operator++(int) {
StepIterator tmp = *this;
++(*this);
return tmp;
}
bool operator==(const StepIterator& other) const {
return current_ == other.current_;
}
bool operator!=(const StepIterator& other) const {
return !(*this == other);
}
private:
Iterator current_;
};
// 使用示例
std::vector<int> vec = {1, 2, 3, 4, 5, 6};
StepIterator<std::vector<int>::iterator> it(vec.begin());
StepIterator<std::vector<int>::iterator> end(vec.end());
for (; it != end; ++it) {
std::cout << *it << " "; // 输出:1 3 5
}
关键点:自定义迭代器必须正确声明iterator_category。这决定了它能用在哪些算法里。我上面声明的是forward_iterator_tag,所以只能用于前向遍历的算法。如果你需要随机访问能力,就得实现+、-、[]等操作。
7.5 迭代器失效问题
嗯,这里要注意。迭代器失效是C++里最隐蔽的坑之一。我当年在线上环境排查过一个bug,查了整整两天,最后发现是迭代器失效导致的。
常见的失效场景:
- vector:插入/删除元素可能导致所有迭代器失效(因为可能重新分配内存)
- deque:在中间插入/删除会使所有迭代器失效
- list:只有被删除的那个迭代器失效,其他安全
- map/set:只有被删除的那个迭代器失效
// 错误示范:迭代器失效
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it == 3) {
vec.erase(it); // it 失效了!
// 继续使用 it 会导致未定义行为
}
}
// 正确做法:利用erase返回下一个有效迭代器
for (auto it = vec.begin(); it != vec.end(); ) {
if (*it == 3) {
it = vec.erase(it); // it 更新为下一个元素
} else {
++it;
}
}
血的教训:我曾经在遍历vector时删除了元素,然后继续用原来的迭代器++,结果程序随机崩溃。调试器里看迭代器指向的内存已经变成乱码了。记住:erase会返回下一个有效迭代器,一定要用它来更新。
迭代器是STL的骨架,理解透了,你写出来的代码才能既高效又安全。下一节我们聊聊算法,到时候你会发现——迭代器分类直接决定了你能用哪些算法,两者是密不可分的。
公众号:蓝海资料掘金营,微信deep3321