🔷 第十三章 STL
📄 一句话概括

输出迭代器是"只能往里写"的迭代器:*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 秒实验

  1. 复活崩溃现场:把 13.3.2 那个 copy(src.begin(), src.end(), bad.begin());(bad 是空 vector)跑一次 → 崩;把第三个参数改成 back_inserter(bad) → 正常。这一对对比就是本节的全部价值。
  2. front_inserter 的顺序:用 deque 试一次 → 顺序同样是反的(每次都插到最前)。
  3. back_inserter 不能给 set:copy(src.begin(), src.end(), back_inserter(s));(s 是 set)→ 编译不过(set 没有 push_back)。set 要用 inserter。
  4. 忘了 <iterator>:把 #include <iterator> 注释掉 → 'ostream_iterator' was not declared。记住这个头文件。

常见误区

  1. "输出迭代器能读"——不能。int x = *out; 编译不过。它只有"写"这一个动作。
  2. "ostream_iterator 的输出和自己手写循环完全一样"——差一点:它在每个元素之后都输出分隔符,所以末尾会多一个(1, 2, 3,)。讲究格式的地方还是得手写。
  3. "back_inserter 什么容器都能用"——要有 push_back:vector / deque / list 可以,set / map / forward_list 不行(要用 inserter 或 front_inserter)。
  4. "front_inserter 也保持原顺序"——不保持(每个都插到最前面,顺序整体反过来)。
  5. "remove 配 back_inserter 就不会崩了"——语法上能编过,但结果是错的:std::remove 会写满整个目标区间(包括末尾的"垃圾"),配 back_inserter 会把垃圾也 push 进去。要"过滤到新容器",用 remove_copy / remove_copy_if。
  6. "输出迭代器能用在算法任一参数位"——只能在"目标位置"那一侧。算法读源数据时必须用普通迭代器。
  7. "插入迭代器有额外开销,能不用就不用"——它确实每次都要调一次 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` 才真删)。** **"要不要保留原数据"是选哪一个的决定因素。**

相关笔记