🔷 第十五章 文件操作
📄 一句话概括

<< 和 >> 不是"语法符号",它们是标准库重载出来的运算符: cout << x 本质是"调用 operator<<(cout, x)"(11.2 讲过"运算符就是函数")。 理解这一点的回报是:<< 能连着写一长串、能接任何类型、能对你的自定义类生效——全都因为它是函数。 本节还带实例01「字符相加并输出」,它把 2.5 的输入输出 和 第三章的类型转换 缝在一起,顺便复习"字符在内存里其实是数字"。

📝 本节和前面几节的关系

前面三节把"流家族"和"常量词汇表"讲完了,本节回到最日常的那两个符号: - 回看 2.5:那时你已经在写 cin >> x,但没人告诉你 >> 是个函数——本节补上; - 回看 11.2.3:<< >> 只能重载成非成员函数(因为左边是 ostream)——那是本章的伏笔回收; - 回看 10.4.4:cout << a << b 能连起来,靠的是 return *this; - 回看 2.3.5 / 2.4.2:实例01 的整个原理就是"char 就是个小整数"。 一句话:本节是"把第二章的日常操作,用第十章和第十一章的知识重新解释一遍"。

一、<< 和 >> 的真面目:两个被重载的运算符

这两个符号原本的意思是位移(3.1 学的:x << 2 是把二进制位左移两位)。

但标准库给它们加了新的重载版本:

cout << x;      // 实际调用的是 operator<<(cout, x)
cin  >> x;      // 实际调用的是 operator>>(cin,  x)

两个名字的来历(叫着舒服,好记):

符号 学名 为什么这么叫
<< 插入运算符(insertion) 把字符插入到输出流里
>> 提取运算符(extraction) 从输入流里提取字符

为什么必须是"非成员函数"? 因为 cout << 5 里,左边是 ostream 对象,不是你的类的对象——你没法跑到 ostream 类里去加成员函数。所以标准库把它写成全局函数(11.2.2 讲过这一条,当时说"<< >> 必须非成员",出处就是这里)。

你能自己做的是"给自己的类重载它":

struct Point { int x, y; };
ostream &operator<<(ostream &out, const Point &p) {
    out << "(" << p.x << "," << p.y << ")";
    return out;                    // ★ 必须返回 out,否则不能连写
}
// 之后就能:cout << p << endl;

二、为什么能"连着写一长串"

cout << "a" << 1 << endl; 这句里,<< 是左结合的,所以实际是:

((cout << "a") << 1) << endl;

要让这成立,必须满足两件事:

  1. operator<< 的返回值是 ostream &(不是 void);
  2. 返回的是"同一个流对象" —— 所以函数最后要写 return out;(成员版本里写 return *this;,10.4.4 讲过)。

一句话:每一节 << 都把"流对象"原样交还给下一节。

这也解释了为什么 endl 能塞在中间:endl 也是一个函数(更准确地说是"函数模板"),它接收流、换行、刷新、再把流还回来。

三、>> 的三条解析规则(2.5 详细讲过,这里复述要点)

输入流读取数据时不是"照单全收",而是按目标类型去解析:

  1. 先跳过前导空白(空格、制表符、换行)——默认行为;
  2. 按目标变量的类型去解析字符:读 int 就找数字、读 double 就找小数、读 string 就取到下一个空白前;
  3. 一旦遇到不匹配的字符就停下,把那个字符留在流里,并且把流置于失败状态(failbit)。

三条例外/变体:

⚠️ `>>` 失败是"罢工"而不是"报错"

int x = 99; cin >> x; 如果你输入 abc: x 还是 99(一个字符都没读进去),流进入失败状态,之后所有 >> 全部无效(除非 clear(),15.4.1 讲)。 这也是"输入必须校验"的根因。

四、实例01:字符相加并输出

教材这道题的本意,是让你看清一件事:

char 在做算术时,用的不是"它长什么样",而是它在内存里存的那个数(ASCII 码)。

#include <iostream>
using namespace std;

int main() {
    char a, b;
    cout << "输入两个字符(用空格分开): ";
    cin >> a >> b;

    cout << "a = " << a << "  它的编码 = " << (int)a << endl;
    cout << "b = " << b << "  它的编码 = " << (int)b << endl;

    int sum = a + b;                 // ★ 相加时自动转成整数:用的是编码
    cout << "相加(按整数输出): " << sum << endl;

    char c = a + b;                  // ★ 存回 char:按单字节截断
    cout << "相加(按字符输出): " << c << "  编码 = " << (int)c << endl;
    return 0;
}

看到什么算成功(输入 3 4):

输入两个字符(用空格分开): 3 4
a = 3  它的编码 = 51
b = 4  它的编码 = 52
相加(按整数输出): 103
相加(按字符输出): g  编码 = 103

数字怎么来的(都验算过):

读结果——这道题只教三句话:

  1. cout << a 输出的是"字符长相",cout << (int)a 输出的才是"里面的数"。 加个 (int) 就能看见真相。
  2. char 参与算术运算时,会先"提升"成 int ——所以 a + b 的结果是 int(这里是 103),不是 char。
  3. 把它塞回 char 才会"按一字节截断" ——103 恰好还在 char 的范围内,所以得到了 'g'。

顺手警告一下:如果你输入 a b(97 + 98 = 195),195 超过了"有符号 char"的上限 127 ——存回 char 就会溢出(在常见平台上会变成负数)。这类例子请不要死记结果:char 到底是有符号还是无符号,由编译器实现决定(2.4.2 讲过)。记住"会溢出"这件事就够了。

五、动手 Demo:三种输入方式对照

>>、getline、get 三兄弟的差别,写一次印象最深。

#include <iostream>
#include <string>
using namespace std;

int main() {
    cout << "输入: 12 34 abc def" << endl;

    int m, n;
    cin >> m >> n;                          // 按空白分词取两个整数
    cout << "① >>        : m=" << m << " n=" << n << endl;

    string rest;
    cin >> rest;                            // 再取一个词
    cout << "② 下一个词  : " << rest << endl;

    // 把这一行剩下的部分读掉(清干净)
    string junk;
    getline(cin, junk);
    cout << "③ 吃掉剩余  : [" << junk << "]" << endl;

    cout << "再输入一整行(可以有空格): ";
    string line;
    getline(cin, line);                     // 读整行(含空格)
    cout << "④ getline   : [" << line << "]" << endl;

    cout << "输入一个字符: ";
    char ch;
    cin.get(ch);                            // 连空白也读
    cin.get(ch);                            // 再读一个
    cout << "⑤ get 两次后: [" << ch << "] (第二个字符)" << endl;
    return 0;
}

看到什么算成功(输入按提示走:先打 12 34 abc def,然后打 hello world,然后打 xy):

输入: 12 34 abc def
① >>        : m=12 n=34
② 下一个词  : abc
③ 吃掉剩余  : [ def]
再输入一整行(可以有空格): hello world
④ getline   : [hello world]
输入一个字符: xy
⑤ get 两次后: [y] (第二个字符)

读结果:

六、30 秒实验

  1. 实例01 里输入 a b → 看 相加(按字符输出) 打出来的是什么(很可能是个怪符号或问号)。体会"溢出"和"实现定义"。
  2. 把 cout << a 改成 cout << a + 0 → 打出来的就变成数字。证明"字符在算术里就是整数"。
  3. 在 Demo 里把 ③ 那段(吃掉剩余)删掉,直接 getline → ④ 那一行读到的是空串。这就是 >> 和 getline 混用的经典事故现场(原因:>> 停在空格前,空格和换行还留在流里,getline 一读就撞上了)。

七、常见误区

  1. "<< 和 >> 是语言的关键字/语法" ——它们是标准库重载的运算符(11.2)。所以你能给自定义类重载它们,也能把 cin/cout 换成文件流。
  2. "cout << a << b 是同时输出两个" ——是"依次输出":左结合,每次 << 都返回同一个流,看起来像一条链。
  3. ">> 遇到空格会读进空格" ——不会,它跳过前导空白(这是默认行为,需要反着来才要 noskipws)。
  4. "cin >> x 失败后重输一次就好了" ——不行,流卡在失败状态了,所有后续 >> 都失效,必须 cin.clear()(15.4.1 讲)。
  5. "char 相加就是把两个字符接起来('3'+'4' 得 '34')" ——完全不对。+ 是算术运算,得到的是数字和(103);想把两个字符接成字符串要 string 或 strcat(7.3 讲过)。
  6. "给自定义类重载 << 时返回值写 void 也行" ——不行,那就 不能连写 cout << a << b。必须返回 ostream &。
自测三问(点开对答案) 1. **`cout << x` 到底调用了什么?为什么能连着写?** ——**调用 `operator<<(cout, x)`**(标准库重载的函数)。**能连写是因为它返回 `ostream &`(同一个流对象)**,左结合之后就串成一条链(10.4.4)。 2. **实例01 里输入 `3 4`,为什么"相加按整数"是 103?再把它存进 `char` 会得到什么?** ——**因为 `'3'` 的编码是 51、`'4'` 是 52,51 + 52 = 103**(**不是 3 + 4 = 7**)。**103 对应字符 `'g'`**(`'a'` 是 97)。**要看清这一点,就把字符 `(int)` 一下。** 3. **`>>` 和 `getline` 混用时,为什么 `getline` 会读到空行?怎么解决?** ——**因为 `>>` 停在空白(空格/换行)之前,那个空白还留在流里**,**`getline` 一上来就撞上它,于是读到空串**。**解法:`>>` 之后先 `getline` 一次把这一行剩下的吃掉**(像 Demo 的 ③)。

相关笔记