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

流(stream)=数据流动的通道。 它把"数据从哪儿来、到哪儿去"这件事抽象掉了——你不需要知道磁盘怎么寻道、怎么分配簇,只管"往流里写 / 从流里读"。 你早就在用它了:cin 是"从键盘流进来",cout 是"往屏幕流出去"。 本章的机关全在一句话里:文件也是一条流。 ——把 cin 换成文件流对象,>> << 照用不误。

📝 本节和前面几节的关系

本节是整章的"总入口",也是给第二章补的一块拼图: - 回看 2.5:那时讲 cin/cout 时说过"这是流",但没定义"流是什么"——这一节补上。 - 回看第十一章:整个流类库就是一棵继承树,ifstream 是 istream 的子类 ——多态在这里第一次从"考试题"变成"天天用的工具"。 - 回看第十章 10.2.2:流对象不可复制(它管着一个真实的系统资源)——那就是浅拷贝的雷换个地方炸。 往前看:本节只讲"流是什么、有哪几种";从 15.2 开始,就是"怎么把文件接到这条流上"。

一、先说个问题:程序算出来的东西,为什么留不住

C++ 里所有变量(不管是栈上的还是 new 出来的)都活在内存里。而内存有个铁律:

进程一结束,内存全部回收。

程序一关,你算了一小时的结果立刻归零。想让数据"活到下一次运行",只有一个办法:

把它写到外部存储(磁盘文件)里。

于是每个要存数据的程序,都逃不掉这件事:

  程序(内存)  ←──→  流对象  ←──→  磁盘上的文件
                 ↑
          这一环就是"流"

本章要学的,就是这个中间的环节。

二、流到底是什么

流(stream)= 数据的传输通道。

这个定义里最值钱的是"通道"两个字。想想现实里的水管:

流就是数据世界里的水管。 数据像水流一样从一端到另一端,你只管两件事:

方向 叫什么 你干的事
数据 流向程序 输入流(input stream) 读
数据 从程序流出 输出流(output stream) 写

而通道的另一头可以接什么?什么都能接:

这就是"抽象"的威力:接口一套,端头随便换。

三、C++ 早就给你准备好了四条标准流

对象 所属类 用途 方向
cin istream 标准输入(键盘) 输入
cout ostream 标准输出(屏幕) 输出
cerr ostream 错误输出 输出
clog ostream 日志输出 输出

前两个在 2.5 就天天写了。后两个就差一件事:

cerr 不带缓冲,clog 带缓冲。

功能一样,"什么时候真正落地"不一样。这两个的区别,正好是下一节要讲的"缓冲"。

四、文件流:把文件接到同一条通道上

C++ 给文件准备了三个类:

类名 能干什么 名字怎么来的
ifstream 只能读 input file stream
ofstream 只能写 output file stream
fstream 又能读又能写 file stream

命名规律一句话:i = 进、o = 出、f = 文件、stream = 流。

现在到了本章最震撼的一张对照表——看看"用文件"和"用键盘屏幕"差多少:

想干的事 第二章(键盘/屏幕) 本章(文件)
读一个整数 cin >> x; in >> x;
读一个字符串 cin >> s; in >> s;
读一整行 getline(cin, line); getline(in, line);
输出一个值 cout << x; out << x;
换行并刷新 cout << endl; out << endl;

只有对象名字换了,语法一个字没变。

这不是巧合,是设计意图(15.1.1 会把背后的继承树画给你看):文件的复杂(打开、定位、关闭、缓冲区)全被藏在对象内部,露在外面的还是那套 << >>。

五、缓冲区:为什么"写完了"却读不到

这是本章第一个"反直觉"的点,先在这儿点破。

每个流对象内部都有一块缓冲区(buffer)——一小片临时存放数据的内存。你调 out << "hello" 的时候:

   你的数据  ──写到──▶  缓冲区(内存,很小很快)  ──攒够/被要求──▶  磁盘(很慢)

"刷"(flush)的时机:

  1. 缓冲区满了;
  2. 你主动要求:out.flush(),或者写 endl(endl = 换行 + 刷);
  3. out.close();
  4. 程序正常结束(流对象析构时自动刷,呼应 10.3);
  5. 有 cin 输入时(防止提示语没打出来就先卡住了)。

为什么要多这一道缓冲? 因为磁盘很慢。

向磁盘写 1 次 1 个字节,和写 1 次 4096 个字节,花的时间差不多。 缓冲区把"一万次小写"攒成"几次大写",性能能差几十倍。

💡 这一条解释了本章两个最常见的怪现象

① "我写完了,程序还开着,另一个程序却读不到内容" ——数据还在缓冲区里没落地。加一句 close() 或 flush() 就好。 ② "同样写文件,用 endl 比用 \n 慢好多" ——endl 每次都要刷一次盘,\n 只是放个换行符在缓冲区里。写大量数据时,\n 明显更快。

六、动手 Demo:一条流,两个"出口"

下面这个小程序,请你重点看函数的参数类型——这是本节的全部价值。

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

// ★ 注意参数:是 ostream&,不是 ofstream&
void report(ostream &out) {
    out << "name: " << "Ada" << endl;
    out << "score: " << 95 << endl;
}

int main() {
    report(cout);                    // ① 流向屏幕

    ofstream file("report.txt");     // ② 流向文件
    if (!file) {                     //    先检查打开成功没有
        cout << "打不开文件" << endl;
        return 1;
    }
    report(file);                    //    同一个函数,喂给它文件流
    file.close();

    cout << "文件已写好,去看 report.txt" << endl;
    return 0;
}

看到什么算成功:

屏幕上出现:

name: Ada
score: 95
文件已写好,去看 report.txt

然后在你的 .cpp 所在文件夹里(Dev-C++ 下就是 exe 旁边)多出一个 report.txt,双击打开,内容是:

name: Ada
score: 95

读结果——这个 Demo 只有一句话值得记:

void report(ostream &out) 这个函数,根本不知道也不关心 out 是屏幕还是文件。

为什么能这样?因为:

这就是整个流类库的设计意图:"写到屏幕"和"写到文件"共用同一份代码。

顺手推一下:以后想加一个"写到内存字符串"(ostringstream),report 一个字都不用改——因为它同样是 ostream 的子类。

七、30 秒实验

  1. 把 report(cout); 注释掉,只留写文件 → 屏幕上只剩最后那句提示。证明两条路互不干扰。
  2. 在 file.close(); 那一行之前,加一句 getchar();(让程序停住),然后赶紧用记事本打开 report.txt → 可能已经能看到内容,也可能空白。能不能看到,取决于缓冲区有没有满 ——这就是"缓冲"最直观的体验。(按回车放程序走完,再看就有内容了。)
  3. 把 report 的形参改成 ofstream &out → report(cout) 立刻编译不过。记住这个不对称:父类能接子类,子类接不了父类。

八、常见误区

  1. "流就是文件" ——不对。流是通道,文件只是它可能接的一端。 键盘、屏幕、字符串、网络都可以接。
  2. "cin/cout 和文件流是两套不同的东西" ——它们共用同一棵继承树、同一套 << >>。本章后面所有的"恍然大悟"都来自这一点。
  3. "写完文件马上就能在记事本里看到" ——不一定,可能还躺在缓冲区里。要确保落地,close() 或 flush()。
  4. "cerr 和 clog 一样,随便用哪个" ——cerr 不缓冲(错误立刻可见),clog 缓冲(日志可以攒)。功能相同,落地时机不同。
  5. "流对象可以像普通变量那样复制、塞进 vector" ——不行,流对象不可复制。 它管着一个真实的系统资源(打开的文件),复制就意味着两个对象管同一份资源——这正是 10.2.2 浅拷贝 + 10.3 重复释放那个雷。要用就传引用(像 Demo 里那样)。
自测三问(点开对答案) 1. **流是什么?为什么说"文件只是一条流"?** ——**流是数据传输的通道**(一种抽象:只关心"进"和"出",不关心两端是谁)。**因为 C++ 把文件也包装成了流对象**(`ifstream`/`ofstream`),**所以 `<<` `>>` 这些语法对文件和屏幕一样有效**——这是本章一切简化的来源。 2. **`report(ostream &out)` 为什么能同时接住 `cout` 和 `ofstream` 对象?** ——**因为 `ofstream` 继承自 `ostream`**(11.1),而**父类的引用可以绑定到子类对象上**(11.4.2)。反过来不行:**形参写成 `ofstream &` 时,`cout` 就传不进去了。** 3. **为什么"刚写完文件,用别的程序打开可能看不到内容"?** ——**数据还在流的缓冲区里**,没落到磁盘。**`close()`、`flush()`、写 `endl`、或程序正常结束**都会把缓冲刷出去。**这也解释了为什么 `endl` 比 `\n` 慢。**

相关笔记