输出迭代器是"只能往里写"的迭代器:*it = x 能写、++it 能前进,但不能读(*it 拿不到值)、而且只能走一遍。它最实用的两个身份是:ostream_iterator(往屏幕/文件"写"数据,可以代替 for 循环打印) 和 back_inserter(往容器尾部"写",会自动 push_back —— 这一下就解决了 13.3.2 那个"copy 到空容器会崩"的问题)。本节实例09 就是"应用输出迭代器"。
13.4 导言立了"五个等级",这一节打开最弱的一级——但它恰恰是前面几处崩溃现场的解药:
- 13.3.2 的 copy:vector<int> dst; copy(src.begin(), src.end(), dst.begin()); → 崩(目标没空间);
- 13.3.4 的 partial_sum:vector<int> ps; partial_sum(a.begin(), a.end(), ps.begin()); → 也崩(同一个原因)。
为什么"输出迭代器"能治它:普通迭代器是"指向一个已经存在的位置",而插入迭代器(back_inserter)的 *it = x 意味着"往容器里塞一个新元素"——写操作本身就完成了"扩容"。这就是"算法改不了容器大小,但插入迭代器能"这个矛盾的正解。
另外 ostream_iterator 也把 13.3.1 那个 for_each 打印 Demo 变得多余——"打印"这件事本身就可以是"往输出流写"的算法。
一、它和普通迭代器差在哪
普通迭代器(比如 vector<int>::iterator):
┌────┬────┬────┬────┐
│ 7 │ 3 │ 9 │ 1 │ 已经存在的 4 个格子
└────┴────┴────┴────┘
↑ it 指向"这里"
*it 读得到值、*it = 5 能改值 ← 读写都行
输出迭代器:
... 已存在的容器 ... ──► [ 新位置 ]
↑ out_it 代表"往这儿塞一个"
*out_it = 5 的意思是"把 5 塞进去" ← 只能写
三条硬限制(记住这三条,就记住了输出迭代器的全部):
| 限制 | 说明 |
|---|---|
| 只能写 | *it = x 合法;int v = *it; 不合法(读不到) |
| 只能前进 | 只能 ++it,不能 --、不能 +n |
| 只能走一遍 | 走过之后不能回头(单遍算法) |
为什么它这么"废"还有用:因为"往目标写东西"这件事根本不需要"读回来"。
copy、fill、transform、partial_sum在目标那一侧都只需要"写"——用输出迭代器当参数,就正好合适。 需求决定接口能力,这是 STL 里反复出现的思路。
二、ostream_iterator:把"往屏幕写"变成迭代器(实例09 的核心)
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator> // ★ 输出迭代器在这个头文件里!
using namespace std;
int main() {
vector<int> v = {7, 3, 9, 1, 5};
// ── ① 传统做法:自己写循环打印 ──────────
cout << "① 手写循环: ";
for (size_t i = 0; i < v.size(); i++) cout << v[i] << " ";
cout << endl;
// ── ② 实例09:用 ostream_iterator 输出 ──
cout << "② 输出迭代器:";
ostream_iterator<int> out(cout, " "); // ★ 绑到 cout,分隔符是空格
copy(v.begin(), v.end(), out); // ★ 把 v "复制到屏幕上"
cout << endl;
// ── ③ 排序 + 输出,一气呵成 ──────────────
vector<int> a = v;
sort(a.begin(), a.end());
cout << "③ 排序后输出:";
copy(a.begin(), a.end(), ostream_iterator<int>(cout, " ")); // ★ 临时对象也能用
cout << endl;
// ── ④ 换成别的分隔符 ────────────────────
cout << "④ 用逗号分隔:";
copy(a.begin(), a.end(), ostream_iterator<int>(cout, ", "));
cout << endl;
// ── ⑤ 从"输入流"读、算、再"写到输出流" ──
// 这两头都是迭代器,中间夹算法 —— 这就是 STL 最漂亮的样子
cout << "⑤ 一行读完再输出(输入几个整数,Ctrl+Z 回车 结束):" << endl;
// istream_iterator<int> in(cin), eof;
// vector<int> nums(in, eof); // 从 cin 读满整个 vector
// sort(nums.begin(), nums.end());
// copy(nums.begin(), nums.end(), ostream_iterator<int>(cout, " "));
// cout << endl;
cout << " (这段代码在下面 13.4.2 会正式讲,先知道"输入也能是迭代器")" << endl;
return 0;
}
看到什么算成功:
① 手写循环: 7 3 9 1 5
② 输出迭代器:7 3 9 1 5
③ 排序后输出:1 3 5 7 9
④ 用逗号分隔:1, 3, 5, 7, 9,
⑤ 一行读完再输出(输入几个整数,Ctrl+Z 回车 结束):
(这段代码在下面 13.4.2 会正式讲,先知道"输入也能是迭代器")
注意几个细节:
| 细节 | 说明 |
|---|---|
ostream_iterator<int> out(cout, " ") |
构造参数是"往哪写"(cout)和"分隔符"(" ") |
copy(v.begin(), v.end(), out) |
语义是"把 v 的内容复制到 out" —— 而 out 就是屏幕。算法没有任何"打印"的代码。 |
| 只能传"目标" | 它出现在参数的第三个位置(目标侧)。源侧永远要普通迭代器。 |
最后一个 ", " 会多打一次 |
输出 1, 3, 5, 7, 9, —— 末尾多一个分隔符。这是 ostream_iterator 的固有行为(它不知道哪个是最后一个)。介意的话,还是得手写循环。 |
这段代码最值得体会的是 ⑤ 那个思路:"输入流 → 算法 → 输出流",全程没写一个
for。 这正是 STL 设计的理想状态:数据从哪来、到哪去,都是迭代器的事;中间怎么处理,是算法的事。#include <iterator>:ostream_iterator/istream_iterator/back_inserter都在这个头文件里,别忘了(和 13.3.4 的<numeric>一样,是"忘了 include"的高发区)。
三、插入迭代器:copy 到空容器不再崩
这是输出迭代器最有实战价值的一面。三个成员:
| 迭代器 | 往哪插 | 对哪些容器有效 |
|---|---|---|
back_inserter(c) |
容器尾部(调 push_back) |
vector、deque、list |
front_inserter(c) |
容器头部(调 push_front) |
deque、list(vector 没有 push_front!) |
inserter(c, it) |
指定位置之前(调 insert) |
所有容器 |
#include <iostream>
#include <vector>
#include <list>
#include <set>
#include <algorithm>
#include <iterator> // ★ back_inserter 在这
using namespace std;
void show(const char *t, const vector<int> &v) {
cout << t << ":";
copy(v.begin(), v.end(), ostream_iterator<int>(cout, " ")); // ★ 顺便一直用它打印
cout << endl;
}
int main() {
vector<int> src = {1, 2, 3};
// ── ① ★ 崩溃现场的解药:copy 到"空容器" ──
vector<int> dst;
copy(src.begin(), src.end(), back_inserter(dst)); // ★ 自动 push_back
show("copy 到空 vector", dst);
// ── ② 对比 13.3.2 那个会崩的写法 ─────────
// vector<int> bad;
// copy(src.begin(), src.end(), bad.begin()); // ❌ 崩:begin() 就是 end(),没位置
// ── ③ 往 list 尾部插 ────────────────────
list<int> l = {9, 8};
copy(src.begin(), src.end(), back_inserter(l));
cout << "copy 到 list 尾部:";
copy(l.begin(), l.end(), ostream_iterator<int>(cout, " "));
cout << endl;
// ── ④ 往 list 头部插(顺序会反过来!)────
list<int> f = {9, 8};
copy(src.begin(), src.end(), front_inserter(f));
cout << "copy 到 list 头部:";
copy(f.begin(), f.end(), ostream_iterator<int>(cout, " "));
cout << " ← 每次插到最前,所以顺序反了" << endl;
// ── ⑤ 往中间插:inserter(容器, 位置) ──────
vector<int> mid = {1, 2, 3};
copy(src.begin(), src.end(), inserter(mid, mid.begin() + 1)); // ★ 插到第 2 个位置之前
show("插到第 2 个位置", mid);
// ── ⑥ 往 set 里插:自动去重 ─────────────
set<int> s = {10, 20};
copy(src.begin(), src.end(), inserter(s, s.begin())); // ★ set 只能用 inserter
cout << "copy 到 set:";
for (int x : s) cout << x << " ";
cout << " ← 自动排序去重(1、2、3 都进不来重复)" << endl;
// ── ⑦ 实用:把"过滤后的结果"直接装进新容器 ──
vector<int> all = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
vector<int> evens;
remove_copy_if(all.begin(), all.end(), back_inserter(evens),
[](int x) { return x % 2 != 0; }); // 把偶数"复制"到 evens
show("过滤出偶数 ", evens);
// ── ⑧ 实用:去重后装进新容器(原容器不动)──
vector<int> dup = {1, 1, 2, 2, 3, 3, 3};
vector<int> uniq;
unique_copy(dup.begin(), dup.end(), back_inserter(uniq));
show("unique_copy ", uniq);
return 0;
}
看到什么算成功:
copy 到空 vector:1 2 3
copy 到 list 尾部:9 8 1 2 3
copy 到 list 头部:3 2 1 9 8 ← 每次插到最前,所以顺序反了
插到第 2 个位置:1 1 2 3 2 3
copy 到 set:1 2 3 10 20 ← 自动排序去重(1、2、3 都进不来重复)
过滤出偶数 :2 4 6 8 10
unique_copy :1 2 3
这张输出里有四个"新技能":
| 现象 | 说明 |
|---|---|
copy 到空 vector 成功 |
back_inserter 把"写"变成了 push_back —— 算法以为自己在"改已存在的元素",其实是在扩容 |
front_inserter 让顺序反过来 |
因为每个元素都插到最前面(3 2 1 9 8)—— 这是 front_inserter 的固有行为,容易看错 |
copy 到 set 自动去重排序 |
因为 inserter 调的是 set::insert——而 set::insert 本来就"自动去重 + 自动排序"(13.2.1) |
remove_copy_if / unique_copy |
带 _copy 后缀的版本:不动原容器,把结果写到目标 —— remove 的"真删版"!(13.3.2 讲的 erase-remove 惯用法,这里出现了"非破坏性"的替代品) |
_copy后缀是"修正序列算法"的第三种形态,值得单独记一行:
想要的效果 原容器要变吗 写法 删除 要变 v.erase(remove(b, e, x), v.end());删除 不变 remove_copy(b, e, out, x);条件删除 要变 v.erase(remove_if(b, e, f), v.end());条件删除 不变 remove_copy_if(b, e, out, f);"要不要保留原数据"这个决定,比"用哪个函数"更重要 —— 这会决定你用
remove还是remove_copy。
四、动手 Demo:把一份数据"流式"处理一遍
任务:用"输入流 → 中间处理 → 输出流"的写法,把一批整数排序后按行输出,全程不写 for 循环。
#include <iostream>
#include <vector>
#include <string>
#include <sstream> // ★ istringstream 在这
#include <algorithm>
#include <iterator>
#include <numeric>
using namespace std;
int main() {
// ── ① 从字符串流读(比 cin 好测试)──────
istringstream data("5 2 8 1 9 3 7");
istream_iterator<int> in(data), eof; // ★ 输入迭代器(13.4.2 详讲)
vector<int> nums(in, eof); // ★ 一句读完整个 vector
cout << "读进来 " << nums.size() << " 个数:";
copy(nums.begin(), nums.end(), ostream_iterator<int>(cout, " "));
cout << endl;
// ── ② 统计(accumulate + 输出迭代器)────
int sum = accumulate(nums.begin(), nums.end(), 0);
cout << "总和 " << sum << ",平均 "
<< (double)sum / nums.size() << endl;
// ── ③ 排序后"写到"新的容器(back_inserter)──
vector<int> sorted;
copy(nums.begin(), nums.end(), back_inserter(sorted));
sort(sorted.begin(), sorted.end());
cout << "排序后:";
copy(sorted.begin(), sorted.end(), ostream_iterator<int>(cout, " "));
cout << endl;
// ── ④ 一行输出"序号 + 值"(用 transform 生成字符串)──
vector<string> lines;
int idx = 1;
for (int x : sorted)
lines.push_back(to_string(idx++) + ". " + to_string(x));
cout << "带序号输出:" << endl;
copy(lines.begin(), lines.end(), ostream_iterator<string>(cout, "\n"));
// ── ⑤ 全部合起来:从输入到输出,只有"算法 + 迭代器"──
cout << "(整个过程:读 → 排 → 输出,中间只有一个 sort)" << endl;
return 0;
}
看到什么算成功:
读进来 7 个数:5 2 8 1 9 3 7
总和 35,平均 5
排序后:1 2 3 5 7 8 9
带序号输出:
1. 1
2. 2
3. 3
4. 5
5. 7
6. 8
7. 9
(整个过程:读 → 排 → 输出,中间只有一个 sort)
copy(..., ostream_iterator<string>(cout, "\n"))那句:分隔符换成"\n"就变成"一个一行"了 ——ostream_iterator的第二个参数可以是任意字符串(空格、逗号、换行都行)。istream_iterator那句更是本节之外的彩蛋:vector<int> nums(in, eof);一句就把整个输入流读成了一个vector—— 这就是 13.4.2 要讲的主角。 有了它,"读一整行数字"这种需求不用再写while (cin >> x)了。
30 秒实验
- 复活崩溃现场:把 13.3.2 那个
copy(src.begin(), src.end(), bad.begin());(bad是空vector)跑一次 → 崩;把第三个参数改成back_inserter(bad)→ 正常。这一对对比就是本节的全部价值。 front_inserter的顺序:用deque试一次 → 顺序同样是反的(每次都插到最前)。back_inserter不能给set:copy(src.begin(), src.end(), back_inserter(s));(s是set)→ 编译不过(set没有push_back)。set要用inserter。- 忘了
<iterator>:把#include <iterator>注释掉 →'ostream_iterator' was not declared。记住这个头文件。
常见误区
- "输出迭代器能读"——不能。
int x = *out;编译不过。它只有"写"这一个动作。 - "
ostream_iterator的输出和自己手写循环完全一样"——差一点:它在每个元素之后都输出分隔符,所以末尾会多一个(1, 2, 3,)。讲究格式的地方还是得手写。 - "
back_inserter什么容器都能用"——要有push_back:vector/deque/list可以,set/map/forward_list不行(要用inserter或front_inserter)。 - "
front_inserter也保持原顺序"——不保持(每个都插到最前面,顺序整体反过来)。 - "
remove配back_inserter就不会崩了"——语法上能编过,但结果是错的:std::remove会写满整个目标区间(包括末尾的"垃圾"),配back_inserter会把垃圾也 push 进去。要"过滤到新容器",用remove_copy/remove_copy_if。 - "输出迭代器能用在算法任一参数位"——只能在"目标位置"那一侧。算法读源数据时必须用普通迭代器。
- "插入迭代器有额外开销,能不用就不用"——它确实每次都要调一次
push_back(可能触发扩容),但它换来的是"不用手写循环 + 不用预知长度"。知道规模时仍可以先reserve再push_back更快 —— 这是"方便"和"极致性能"的取舍。
自测三问(点开对答案)
1. **输出迭代器的三条限制是什么?为什么这么"弱"还有用?**——**只能写、只能前进、只能走一遍**。**因为"往目标写东西"这个动作本身不需要读回来**——`copy` / `fill` / `transform` 在目标一侧只需要"写"。**需求决定接口能力**,这就是它存在的原因。 2. **`copy` 到空容器为什么崩?`back_inserter` 怎么救的?**——因为 **`dst.begin()` 在空容器里就是 `end()`,没有可写的位置**。**`back_inserter(dst)` 把"写"实现成了 `push_back`** —— **写操作本身就完成了扩容**,于是"算法改不了容器大小"这个局限被绕开了。**注意:`back_inserter` 要求容器有 `push_back`(`set` 不行,要用 `inserter`)。** 3. **想"过滤到新容器、保留原数据",用哪个函数?**——用带 `_copy` 后缀的版本:**`remove_copy` / `remove_copy_if`**(把留下的元素**复制**到目标容器,原容器不动)。**对比 `remove` / `remove_if` 是就地挪(还得配 `erase` 才真删)。** **"要不要保留原数据"是选哪一个的决定因素。**- ☐ 下一节(13.4.2 输入迭代器)