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

输入迭代器是"只能往外读"的迭代器:*it 能取值、++it 能前进,但不能写(*it = x 不合法)、只能走一遍。它最实用的身份是 istream_iterator ——把"从输入流读数据"变成迭代器,于是 vector<int> v(in, eof); 一句就能把一个输入流读成一个容器,while (cin >> x) v.push_back(x); 那套循环就退休了。 一句话记两者的分工:输入迭代器是"数据的来路",输出迭代器是"数据的去路" ——中间夹着算法,这就是 STL 的"流水线"。

📝 本节和前面几节的关系

上一节(13.4.1)讲了"只能写"的输出迭代器,这一节讲它对称的另一半:"只能读"的输入迭代器。 - 13.4.1 的 Demo ④ 已经预告过它(istream_iterator 读 istringstream),这一节正式讲; - 它和 13.3 那些"只读一遍"的算法天然匹配:find、count、accumulate 只需要"能读、能 ++" ——所以它们能直接作用在"从输入流读来的数据"上,数据不用先全部装进容器; - 往回看 2.5.2 C++语言中的流:那里讲的是 cin / cout 这个"流"的概念,这一节把它接到了 STL 上**。

一、它和输出迭代器正好相反

输出迭代器(13.4.1) 输入迭代器(本节)
读写 只能写(*it = x) 只能读(x = *it)
前进 只能 ++ 只能 ++
遍数 单遍 单遍
典型 ostream_iterator、back_inserter istream_iterator
在算法里的位置 "目标"那一侧(第 3 个参数) "源"那一侧(前两个参数)

这两个合起来就构成了完整的"流水线": 输入迭代器 ──► 算法 ──► 输出迭代器 (数据从哪来) (怎么处理) (数据到哪去) istream_iterator ostream_iterator vector.begin() back_inserter 上一节讲过"数据从哪来、到哪去都是迭代器的事"——这一节补齐了"从哪来"。

二、istream_iterator:把输入流变成迭代器

#include <iostream>
#include <vector>
#include <string>
#include <sstream>
#include <iterator>              // ★
#include <algorithm>
#include <numeric>
using namespace std;

int main() {
    // ── 用字符串流当"输入源"(方便测试;真实场景可以换成 cin 或文件流)──
    istringstream data("5 2 8 1 9 3 7");

    // ── ① 一句读完整个流 ────────────────────
    istream_iterator<int> in(data);       // ★ 绑到输入流
    istream_iterator<int> eof;            // ★ 默认构造 = "流的末尾"
    vector<int> nums(in, eof);            // ★ 用一对迭代器构造 vector
    cout << "读进来 " << nums.size() << " 个数:";
    for (int x : nums) cout << x << " ";
    cout << endl;

    // ── ② 对比传统写法(也是对的,只是更长)──
    istringstream data2("5 2 8 1 9 3 7");
    vector<int> nums2;
    int x;
    while (data2 >> x) nums2.push_back(x);       // ★ 老写法
    cout << "传统写法也读了 " << nums2.size() << " 个" << endl;

    // ── ③ 输入迭代器能直接喂给算法 ──────────
    istringstream data3("5 2 8 1 9 3 7");
    istream_iterator<int> in3(data3), eof3;
    cout << "直接累加(accumulate):"
         << accumulate(in3, eof3, 0) << "  ← 数据没进过任何容器!" << endl;

    // ── ④ 读字符串(按"空白"切分)──────────
    istringstream words("the quick brown fox");
    istream_iterator<string> wi(words), we;
    vector<string> ws(wi, we);
    cout << "读到 " << ws.size() << " 个词:";
    for (const string &w : ws) cout << w << " ";
    cout << endl;

    // ── ⑤ 实用:一边读一边找(读一半就停)──
    istringstream data4("10 20 30 40 50 60");
    istream_iterator<int> in4(data4), eof4;
    istream_iterator<int> found = find(in4, eof4, 40);   // ★ 找到 40 就停
    if (found != eof4)
        cout << "找到了:" << *found << "(后面还有 "
             << distance(found, eof4) << " 个没读)" << endl;
    return 0;
}

看到什么算成功:

读进来 7 个数:5 2 8 1 9 3 7 
传统写法也读了 7 个
直接累加(accumulate):35  ← 数据没进过任何容器!
读到 4 个词:the quick brown fox 
找到了:40(后面还有 2 个没读)

四个要点:

要点 说明
istream_iterator<int> eof; 默认构造出来的就是"流的末尾" —— 它是所有输入流的"通用终点"(和容器的 end() 对应)
vector<int> v(in, eof); 一句读完(用区间构造函数)。它读到流结束或类型不匹配为止
能直接喂算法 accumulate(in3, eof3, 0) —— 数据从没进过容器,这就是"流式处理"
按"空白"切分 >> 的规矩:遇空格/制表/换行都算一个词的结束(2.5.2 讲过的流的行为)

③ 那句特别值得停一下:accumulate 直接从输入流里累加,一个容器都没用到。 如果数据量很大(比如几千万行日志),这就省掉了"先把全部读进内存"的开销 —— 这就是"流式算法"的价值。 ⑤ 那句也是:find 找到就停,后面的数据根本不会被读(distance 只是数了"还剩几个",并不真的读完)。"按需读取"在处理大文件时非常关键。

三、输入迭代器的"单遍"是什么意思

"单遍"(single-pass)是输入迭代器最容易被忽略、也最容易踩的限制:

#include <iostream>
#include <sstream>
#include <iterator>
#include <vector>
using namespace std;

int main() {
    istringstream data("1 2 3 4 5");

    // ❌ 看着像"能读好几遍",其实不行
    istream_iterator<int> in(data), eof;
    // 第一遍:读完了
    int sum1 = 0;
    for (istream_iterator<int> it = in; it != eof; ++it) sum1 += *it;
    cout << "第一遍累加:" << sum1 << endl;

    // 第二遍:in 已经走到末尾了,什么也读不到
    int sum2 = 0;
    for (istream_iterator<int> it = in; it != eof; ++it) sum2 += *it;
    cout << "第二遍累加:" << sum2 << "  ← 流已经被读光了!" << endl;

    cout << "(这就是单遍:输入迭代器只能走一次,不能回头重读)" << endl;
    return 0;
}

看到什么算成功:

第一遍累加:15
第二遍累加:0  ← 流已经被读光了!
(这就是"单遍":输入迭代器只能走一次,不能回头重读)

为什么"读过了就没了":输入流是"一次性的" —— 读过的那几个字符已经不在缓冲区里了(2.5 讲的"流"就是这个性质)。 所以"单遍"不是"输入迭代器故意小气",而是"它背后的东西本来就不能重读"。 同理,输出迭代器也是单遍(写出去的东西不能"取消")。 对比一下:vector 的迭代器是"多遍"的 —— 因为数据实实在在存在内存里,你想读几遍都行。"多遍"是"随机访问迭代器"的前置条件(13.4.3 / 13.4.5)。

四、动手 Demo:一个"边读边算"的小工具

任务:读一串成绩,不把全部数据装进容器,直接算出总分、最高分、平均分。

#include <iostream>
#include <sstream>
#include <iterator>
#include <vector>
#include <algorithm>
#include <numeric>
using namespace std;

int main() {
    istringstream data("88 95 72 88 60 95 88 100 55");

    // ── ① 直接累加(不建容器)────────────
    istream_iterator<int> in1(data), eof;
    int total = accumulate(in1, eof, 0);
    cout << "① 总分 = " << total << "(一个容器都没建)" << endl;

    // ── ② 想知道"个数"和"最大",就得重新读一遍流 ──
    //    (因为输入迭代器是单遍的:上面已经读光了)
    istringstream data2("88 95 72 88 60 95 88 100 55");
    istream_iterator<int> in2(data2), eof2;

    int n = 0, mx = -1, mn = 1000;
    for (istream_iterator<int> it = in2; it != eof2; ++it) {
        n++;
        if (*it > mx) mx = *it;
        if (*it < mn) mn = *it;
    }
    cout << "② 共 " << n << " 人,最高 " << mx << ",最低 " << mn << endl;

    // ── ③ 折中:先装进容器,再想读几遍读几遍 ──
    istringstream data3("88 95 72 88 60 95 88 100 55");
    istream_iterator<int> in3(data3), eof3;
    vector<int> scores(in3, eof3);            // ★ 装一次
    cout << "③ 装进 vector 后(" << scores.size() << " 个):" << endl;
    cout << "   总分 = " << accumulate(scores.begin(), scores.end(), 0) << endl;
    cout << "   最大 = " << *max_element(scores.begin(), scores.end()) << endl;
    cout << "   平均 = " << (double)accumulate(scores.begin(), scores.end(), 0) / scores.size() << endl;
    cout << "   (想读几遍读几遍——这就是容器比流多的东西)" << endl;
    return 0;
}

看到什么算成功:

① 总分 = 741(一个容器都没建)
② 共 9 人,最高 100,最低 55
③ 装进 vector 后(9 个):
   总分 = 741
   最大 = 100
   平均 = 82.3333
   ("想读几遍读几遍"——这就是容器比流"多"的东西)

这段 Demo 把"流 vs 容器"的取舍讲明白了:

流(输入迭代器) 容器(vector 等)
内存 几乎不占(读一个处理一个) 要装下全部数据
能读几遍 只有一遍 想读几遍读几遍
能随机访问 ❌ ✅(随机访问迭代器)
能排序 ❌(不能回头) ✅
适用 大数据量、一次扫过就算完 需要反复访问/排序/查找

现实里最常用的组合是"两者结合":用 istream_iterator 一句读进来,再用容器的各种能力去处理(Demo ③)。"全流式"只在数据大到装不进内存时才必须用。

30 秒实验

  1. 单遍验证:连续对同一个 istream_iterator 做两次累加 → 第二次是 0。
  2. 读到类型不匹配就停:istringstream data("1 2 abc 4"); 然后 vector<int> v(in, eof); → 只有 1、2 两个(遇到 abc 停止)。这是 >> 的行为。
  3. find 会"提前停":在 istringstream 里塞一万个数,find 找第 3 个 → 瞬间返回(后面的没读)。
  4. 忘了 <iterator>:注释掉 → 'istream_iterator' was not declared。

常见误区

  1. "输入迭代器能写"——不能(*it = x 不合法)。要写用输出迭代器或普通迭代器。
  2. "输入迭代器能读好几遍"——只能一遍。读第二遍是空的。要反复访问,先把数据装进容器。
  3. "istream_iterator 会自己判断类型对不对"——它靠 >> 的规则:遇到不符合目标类型的内容就停止(1 2 abc 4 只能读到 1、2)。它不会"跳过"错误继续读。
  4. "eof 要绑到某个流"——不用,默认构造的 istream_iterator<T> 就是"所有流的末尾"(它是个固定的"哨兵")。
  5. "读完再读就是又从头开始"——不是"从头开始",是"什么都没有"。流的位置只会往前走,不会回退(要回退得用 seekg,那是另一套 API)。
  6. "istream_iterator 读 cin 最方便"——方便但要小心:它默认会跳过所有空白(包括换行),所以"按行读"要用 getline(那是另一个工具)。istream_iterator 是"按"词"读",不是"按行读"。
自测三问(点开对答案) 1. **输入迭代器和输出迭代器分别是什么?它们在算法参数里各占哪一边?**——**输入迭代器只能读**(`x = *it`),**`istream_iterator` 是典型**;**输出迭代器只能写**(`*it = x`),**`ostream_iterator` / `back_inserter` 是典型**。**输入迭代器出现在算法的"源"侧(前两个参数),输出迭代器出现在"目标"侧(第三个参数)。** 两者都只能 `++`、都是单遍。 2. **"单遍"是什么意思?为什么输入迭代器只能单遍?**——**走一次就没了,不能回头重读**。**因为输入流本身就是一次性的**:读过的字符已经不在缓冲区了。**要读好几遍,必须先把数据装进容器**(容器的迭代器是"多遍"的)。 3. **`vector v(in, eof);` 这一句干了什么?什么时候该用、什么时候不该用?**——它**把从输入流读到的所有整数装进 `v`**(用一对迭代器构造区间)。**该用**:一次读完、后续要反复访问/排序/查找。**不该用**:数据量大到装不下内存、或者只需要"扫一遍算个总和"——那时**直接把 `istream_iterator` 喂给 `accumulate` / `find` 更省内存**。

相关笔记