流(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带缓冲。
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)的时机:
- 缓冲区满了;
- 你主动要求:
out.flush(),或者写endl(endl= 换行 + 刷); out.close();- 程序正常结束(流对象析构时自动刷,呼应 10.3);
- 有
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 是屏幕还是文件。
为什么能这样?因为:
cout是一个ostream对象;ofstream是ostream的子类(11.1);- 父类的引用可以绑定到子类对象上(11.4.2)。
这就是整个流类库的设计意图:"写到屏幕"和"写到文件"共用同一份代码。
顺手推一下:以后想加一个"写到内存字符串"(ostringstream),report 一个字都不用改——因为它同样是 ostream 的子类。
七、30 秒实验
- 把
report(cout);注释掉,只留写文件 → 屏幕上只剩最后那句提示。证明两条路互不干扰。 - 在
file.close();那一行之前,加一句getchar();(让程序停住),然后赶紧用记事本打开report.txt→ 可能已经能看到内容,也可能空白。能不能看到,取决于缓冲区有没有满 ——这就是"缓冲"最直观的体验。(按回车放程序走完,再看就有内容了。) - 把
report的形参改成ofstream &out→report(cout)立刻编译不过。记住这个不对称:父类能接子类,子类接不了父类。
八、常见误区
- "流就是文件" ——不对。流是通道,文件只是它可能接的一端。 键盘、屏幕、字符串、网络都可以接。
- "
cin/cout和文件流是两套不同的东西" ——它们共用同一棵继承树、同一套<<>>。本章后面所有的"恍然大悟"都来自这一点。 - "写完文件马上就能在记事本里看到" ——不一定,可能还躺在缓冲区里。要确保落地,
close()或flush()。 - "
cerr和clog一样,随便用哪个" ——cerr不缓冲(错误立刻可见),clog缓冲(日志可以攒)。功能相同,落地时机不同。 - "流对象可以像普通变量那样复制、塞进
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` 慢。**- ☐ 下一节(15.1.1 C++中的流类库)
相关笔记
- 数据的输入与输出 ——
cin/cout的第一次登场 - C++语言中的流 —— 那时第一次提到"流"这个词
- iostream 标准库 ——
cin/cout/cerr/clog四个对象的老家 - 继承 —— 流类库这棵继承树的地基
- 利用虚函数实现动态绑定 —— 父类引用绑定子类对象
- 复制构造函数 —— 流对象为什么不能复制
- C++中的流类库 —— 下一节:把这棵树画全
- 🔷 C++基础 · 索引 —— 全课路线图