前向迭代器是"能读能写、能走多遍,但只能往前走"的迭代器。它比输入迭代器多了一项关键能力——多遍(读过之后还能再读一遍,因为数据在内存里);比双向迭代器少一项——不能 --。典型代表是 forward_list(单向链表)和 unordered_set / unordered_map(哈希表)。
它存在的意义:很多算法需要"扫两遍"(先算个数、再处理;或者先找位置、再修改),输入迭代器做不到,前向迭代器就能。
13.4.1 / 13.4.2 讲的两个是"只能走一遍"的(流式);这一节开始进入"内存里的迭代器":
流式的(单遍) 内存里的(多遍)
────────────── ──────────────
输出(只写) ←独立一支 前向(只 ++)
输入(只读) ─┐ 双向(++ --)
└───────────► 随机访问(+n -n [] <)
"多遍"是这一节的分水岭:能走多遍的前提是"数据实实在在存在某处"——而这正是"容器"和"流"的本质区别(13.4.2 的 Demo 里刚对比过)。
顺带说一句:forward_list 是 13.1 没讲过的第四个序列容器(单向链表,比 list 更省内存);unordered_set / unordered_map 是 13.2 提过的"哈希表版"容器。它们的迭代器都只有"前向"这一级。
一、前向迭代器的"能力清单"
| 操作 | 输入迭代器(13.4.2) | 前向迭代器(本节) | 双向(13.4.4) |
|---|---|---|---|
*it 读 |
✅ | ✅ | ✅ |
*it = x 写 |
❌ | ✅ | ✅ |
++it |
✅ | ✅ | ✅ |
--it |
❌ | ❌ | ✅ |
+n / -n |
❌ | ❌ | ❌ |
| 能走多遍 | ❌ 单遍 | ✅ 多遍 | ✅ 多遍 |
能不能比大小(<) |
❌ | ❌ | ❌ |
一句话:前向迭代器 = 输入迭代器 + 写能力 + 多遍。
"多遍"为什么这么重要:因为很多算法内部要"走两趟": -
remove要先找到第一个要删的,再把后面的往前挪; -unique要"比相邻的一对"(得同时持有两个位置); -partition要反复交换前后元素。输入迭代器一个都做不了(走一遍就没了),前向迭代器就够用了(可以反反复复走)。
二、forward_list:最省的链表
先用一段代码看看它长什么样、和 list 差在哪:
#include <iostream>
#include <forward_list>
#include <list>
#include <iterator>
#include <algorithm>
using namespace std;
int main() {
// ── ① 基本操作 ────────────────────────
forward_list<int> fl = {3, 1, 4, 1, 5};
forward_list<int> fl2 = {9, 2};
// 它只有 push_front / pop_front(没有 push_back!)
fl.push_front(7);
fl.pop_front();
cout << "fl:";
for (int x : fl) cout << x << " ";
cout << endl;
// ── ② 在"某位置之后"插入(注意:是"之后")──
forward_list<int>::iterator it = fl.begin();
++it; // 走到第二个
fl.insert_after(it, 99); // ★ 插在 it 之后
cout << "insert_after 之后:";
for (int x : fl) cout << x << " ";
cout << endl;
// ── ③ 删除"某位置之后"的那个 ────────────
fl.erase_after(it);
cout << "erase_after 之后: ";
for (int x : fl) cout << x << " ";
cout << endl;
// ── ④ 它自带的"链表专属技能"(和 list 一样)──
forward_list<int> f3 = {1, 2, 3, 2, 4, 2, 5};
f3.remove(2);
cout << "remove(2) 之后: ";
for (int x : f3) cout << x << " ";
cout << endl;
forward_list<int> f4 = {5, 2, 8, 1, 9, 3};
f4.sort();
cout << "sort() 之后: ";
for (int x : f4) cout << x << " ";
cout << endl;
// ── ⑤ 和 list 的关键差别:内存更省,但没有 -- 和双向遍历 ──
cout << "-- forward_list vs list --" << endl;
cout << "sizeof(forward_list<int>::iterator) 通常比 list 小(少一个前驱指针)" << endl;
// cout << *(--fl.end()); // ❌ 编译不过:前向迭代器不能 --
return 0;
}
看到什么算成功:
fl:3 1 4 1 5
insert_after 之后:3 99 1 4 1 5
erase_after 之后: 3 1 4 1 5
remove(2) 之后: 1 3 4 5
sort() 之后: 1 2 3 5 8 9
-- forward_list vs list --
sizeof(forward_list<int>::iterator) 通常 < list(少一个"前驱"指针)
它和 list(13.1.3)的差别表:
forward_list(单向链表) |
list(双向链表) |
|
|---|---|---|
| 每个节点存几个指针 | 1 个(只指向后一个) | 2 个(前驱 + 后继) |
| 内存 | 更省 | 更费 |
| 迭代器等级 | 前向(只 ++) |
双向(++ / --) |
push_back / pop_back |
没有(要走到尾部是 O(n)) | ✅ |
| 插入/删除的 API | insert_after / erase_after(只给"之后") |
insert / erase(给"之前") |
size() |
没有(要自己数,或 distance) |
✅ |
能用 reverse 算法吗 |
❌ 不能(要 --) |
✅ |
自带 sort / remove / unique |
✅ | ✅ |
forward_list的两个"反直觉"设计值得单独说: 1. 没有push_back、没有size()—— 为了省那一个指针的空间,它愿意牺牲这些便利。这就是"极致省内存"的取舍(现实里它少用于一般业务,多用于内存紧张的场景)。 2. 插入删除都是insert_after/erase_after—— 因为向前走容易、回头难,所以"给你一个位置,我只能在它后面动手"。这也解释了为什么list的 API 是insert(it, x)(在it之前插入)而forward_list是insert_after(it, x)。
三、unordered_set / unordered_map:哈希表的迭代器也是前向的
13.2.1 和 13.2.3 提过这两个名字("只要快速去重/查找、不需要有序,用哈希表版更快")。它们的迭代器等级就是前向的:
#include <iostream>
#include <string>
#include <sstream>
#include <vector>
#include <forward_list>
#include <list>
#include <set>
#include <unordered_set>
#include <unordered_map>
#include <iterator>
#include <algorithm>
using namespace std;
int main() {
// ── ① unordered_set:去重,但"无序" ─────
unordered_set<string> us = {"banana", "apple", "cherry", "apple"};
cout << "unordered_set 里 " << us.size() << " 个(apple 重复被去掉):";
for (const string &s : us) cout << s << " ";
cout << endl;
cout << "(注意顺序:不是字母序,是哈希桶序 —— 和 set 比一眼就看出来)" << endl;
// ── ② 和 set 对比 ─────────────────────
set<string> s = {"banana", "apple", "cherry"};
cout << "set 的顺序(有序):";
for (const string &x : s) cout << x << " ";
cout << endl;
// ── ③ unordered_map:计数(13.2.3 的写法照搬)──
unordered_map<string, int> cnt;
string text = "the quick brown fox the dog the cat";
stringstream ss(text);
string w;
while (ss >> w) cnt[w]++;
cout << "词频(unordered_map):" << endl;
for (const auto &p : cnt) cout << " " << p.first << " -> " << p.second << endl;
// ── ④ 它的迭代器只有"前向":不能 -- ──────
// auto it = us.end(); --it; // ❌ 编译不过
// sort(us.begin(), us.end()); // ❌ 编译不过(要随机访问,而且是只读的)
cout << "(哈希表的迭代器是前向的:不能 --,也不能 sort)" << endl;
// ── ⑤ 那它强在哪?—— 查找快 ────────────
cout << "-- 查找 --" << endl;
// set::find → O(log n)(红黑树,一路比较下去)
// unordered_set::find → 平均 O(1)(算哈希、直接跳过去)
cout << "us.count(\"apple\") = " << us.count("apple") << endl;
cout << "(有序 vs 快速:要按顺序遍历就选 set;只要快速判断在不在就选 unordered_set)" << endl;
return 0;
}
看到什么算成功:
unordered_set 里 3 个(apple 重复被去掉):cherry banana apple
(注意顺序:不是字母序,是哈希桶序 —— 和 set 比一眼就看出来)
set 的顺序(有序):apple banana cherry
词频(unordered_map):
cat -> 1
the -> 3
dog -> 1
brown -> 1
quick -> 1
fox -> 1
(哈希表的迭代器是前向的:不能 --,也不能 sort)
-- 查找 --
us.count("apple") = 1
(有序 vs 快速:要"按顺序遍历"选 set;只要"快速判断在不在"选 unordered_set)
"有序版"和"哈希版"的取舍:
set / map |
unordered_set / unordered_map |
|
|---|---|---|
| 底层 | 平衡二叉搜索树(红黑树) | 哈希表 |
| 元素顺序 | 按 key 有序 | 无序(哈希桶顺序,会随插入变化) |
| 查找 | O(log n) | 平均 O(1),最坏 O(n) |
| 迭代器等级 | 双向(13.4.4) | 前向(本节) |
能不能 sort |
❌ | ❌ |
| 内存 | 稍省(每节点常数开销) | 稍费(要维护桶数组) |
| 什么时候用 | 要按顺序遍历 · 要范围查询(lower_bound) |
只要快速去重/查找 · 不关心顺序 |
"平均 O(1)"里的"平均"要留意:哈希冲突严重时(比如所有 key 都落进同一个桶)会退化成 O(n)。 现实里哈希函数一般都够好,所以"平均 O(1)"基本成立。但如果你要"性能绝对有保证",
set的 O(log n) 反而是更稳的选择 —— 这也是"最坏情况"和"平均情况"的取舍。
四、动手 Demo:什么时候"前向"就够、什么时候必须"双向"
任务:用 forward_list 和 list 各做一遍"遍历 + 处理",找出前向迭代器的边界。
#include <iostream>
#include <forward_list>
#include <list>
#include <algorithm>
#include <numeric>
#include <iterator>
using namespace std;
template <typename C>
void show(const char *t, const C &c) {
cout << t << ":";
for (int x : c) cout << x << " ";
cout << endl;
}
int main() {
forward_list<int> fl = {3, 1, 4, 1, 5, 9, 2, 6};
// ── ① 前向能做:遍历、累加、找、按条件删 ──
show("原始 ", fl);
cout << "① 累加(accumulate):" << accumulate(fl.begin(), fl.end(), 0) << endl;
forward_list<int> f2 = fl;
f2.remove_if([](int x) { return x > 4; }); // ✅ 成员函数(真删)
show("① 删掉 >4 之后", f2);
// 算法版也能用(只要"能 ++")
forward_list<int> f3 = fl;
cout << "① count(1) = " << count(f3.begin(), f3.end(), 1) << "(算法也能用)" << endl;
// ── ② 前向不能做:reverse(要 --)、sort(要 +n)、size(没有)──
// reverse(fl.begin(), fl.end()); // ❌ 要 --(双向)
// sort(fl.begin(), fl.end()); // ❌ 要随机访问
// cout << fl.size(); // ❌ 没有 size()
cout << "② 不能用 reverse/sort,也没有 size()(要自己 distance)" << endl;
cout << " 自己数一遍:" << distance(fl.begin(), fl.end()) << " 个" << endl;
// ── ③ 想"倒着看"?先复制到 vector ─────────
vector<int> v(fl.begin(), fl.end());
reverse(v.begin(), v.end());
show("③ 倒过来(借 vector)", v);
// ── ④ 双向的 list 就能直接 reverse ────────
list<int> l = {3, 1, 4, 1, 5};
reverse(l.begin(), l.end());
show("④ list 直接 reverse", l);
cout << "-- 结论 --" << endl;
cout << "前向迭代器:能遍历、能读写、能多遍 —— 足够对付只看一遍/顺着一遍的算法" << endl;
cout << "要倒着走或跳着走,就得升级到双向 / 随机访问" << endl;
return 0;
}
看到什么算成功:
原始 :3 1 4 1 5 9 2 6
① 累加(accumulate):31
① 删掉 >4 之后:3 1 4 1 2
① count(1) = 2(算法也能用)
② 不能用 reverse/sort,也没有 size()(要自己 distance)
自己数一遍:8 个
③ 倒过来(借 vector):6 2 9 5 1 4 1 3
④ list 直接 reverse:5 1 4 1 3
-- 结论 --
前向迭代器:能遍历、能读写、能多遍 —— 足够对付"只看一遍/顺着一遍"的算法
要"倒着走"或"跳着走",就得升级到双向 / 随机访问
这个 Demo 里有个实用技巧:"先把数据从弱迭代器的容器复制到
vector,再用强大的算法" ——vector<int> v(fl.begin(), fl.end());(用区间构造一次搞定)。这是"容器能力不够时的标准变通手段"(13.1.1 区间构造的用法)。 但也要想清楚代价:复制一份数据要花时间和内存。如果这是热点代码,就得重新考虑"是不是一开始就该用vector"。
30 秒实验
- 验证"没有
size()":fl.size()→ 编译不过。用distance(fl.begin(), fl.end())(O(n))。 - 验证"不能
--":auto it = fl.end(); --it;→ 编译不过。 insert_aftervsinsert:fl.insert_after(fl.begin(), 99);插在第一个元素之后;l.insert(l.begin(), 99);插在第一个元素之前。这两个 API 的差别背后就是"能不能回头"。unordered_set的顺序:插1..10再遍历一次 → 基本不会是1 2 3 ...(哈希桶序)。和set一起跑一下,差别一眼可见。
常见误区
- "前向迭代器就是输入迭代器"——多两项能力:能写(
*it = x)和能多遍。输入迭代器是"流式"的(读一遍就没了)。 - "前向迭代器能
--"——不能。要--得用双向迭代器(13.4.4)。 - "
forward_list和list用法差不多"——差得多:forward_list没有push_back/pop_back/size(),插入删除是insert_after/erase_after(在位置之后)。 它的目的是极致省内存。 - "
forward_list适合替换vector来省内存"——要看需求:它就是"单链表",只在"频繁在已知位置插入删除 + 内存紧张"时才合适;一般业务里vector几乎总是更好的默认选择(13.1.1)。 - "
unordered_map比map全面更好"——不是:它无序、迭代器只有前向、不能做范围查询(没有lower_bound)。要"按顺序遍历"或"查一段区间",还得用map。 - "哈希表是 O(1),所以一定快"——平均 O(1),最坏 O(n)。特别在意最坏情况时,
set/map的 O(log n) 更可预测。 - "
unordered_*的迭代器顺序稳定"——不稳定:重新哈希(插入变多)会改变元素的相对顺序。别写依赖顺序的代码。
自测三问(点开对答案)
1. **前向迭代器比输入迭代器多了什么?又比双向少了什么?**——**多了"能写"和"能多遍"**(输入迭代器只读、只能走一遍);**少了"`--`"**(双向能后退)。**所以它适合"能反反复复向前扫、需要修改元素"的算法。** 2. **`forward_list` 和 `list` 的关键差别有哪些?**——**`forward_list` 是单向链表**:**只存一个指针(省内存)、迭代器只有前向、没有 `push_back`/`pop_back`/`size()`、插入删除用 `insert_after`/`erase_after`。**`list` 是双向链表,上面这些都有。**`forward_list` 是"用便利换内存"。** 3. **`unordered_set` / `unordered_map` 和 `set` / `map` 怎么选?**——**要"按 key 顺序遍历"或"范围查询"→ `set` / `map`**(底层红黑树,O(log n),迭代器双向);**只要"快速判断在不在 / 快速查值"、不关心顺序 → `unordered_*`**(哈希表,**平均 O(1)**,迭代器只有前向)。**注意 `unordered_*` 最坏会退化成 O(n),且遍历顺序不稳定。**- ☐ 下一节(13.4.4 双向迭代器)