C++ 的流不是"一个类",而是一整个家族。整个家族挂在一棵继承树上:
ios(公共祖先)→ istream / ostream(方向分家)→ 各个具体流(文件 / 字符串 / 键盘屏幕)。
看得懂这棵树,就明白了两件事:① 为什么换个对象、代码不用改;② 为什么 ios 里那些"状态检查"和"格式控制"哪个流都能用。
一句话概括:"通道"这个抽象,是用继承实现的——上层管通用规则,下层管具体去哪儿。
15.1 说了"流是通道",本节把这棵树的骨架画出来,为下面几件具体的事做铺垫:
- 15.1.3 讲的 ios 枚举常量,就藏在树根那一层——所以它是"所有流共享"的;
- 15.4.1 讲的状态位(good/fail/eof)也在树根那一层——同样所有流共享;
- 第十一章的多重继承(11.3)在这里第一次见到"标准库也在用":iostream 就是 istream + ostream 双继承来的。
一句话:本节是"流"的族谱;后面所有小节,都是在族谱上找对应的人。
一、四个头文件,各管一段
流家族的第一件事是分家——不同用途的东西放在不同头文件里:
| 头文件 | 提供的东西 | 你用的场景 |
|---|---|---|
<iostream> |
cin / cout / cerr / clog |
键盘与屏幕(2.5 就在用) |
<fstream> |
ifstream / ofstream / fstream |
文件(本章主角) |
<sstream> |
istringstream / ostringstream / stringstream |
把字符串当流用(拆解、拼接文本) |
<iomanip> |
setw / setprecision / fixed / setfill |
控制"怎么写好看"(2.5.3 用过) |
四件套的分工,一句话记住:
<iostream>:人和程序对话;<fstream>:程序的磁盘;<sstream>:程序自己的内存;<iomanip>:写作颜(格式)。
一个常见的报错:
#include <iostream>之后写ifstream,报'ifstream' was not declared in this scope。 因为ifstream不在<iostream>里,必须单独#include <fstream>。 这个错误在本章会不止一次遇到。
二、族谱:流类库的继承树
这是本章最重要的一张图,请务必看懂它。
ios ← 所有流的公共祖先(状态 · 格式 · 缓冲)
┌─────────┴─────────┐
istream ostream ← 第二层:方向分家(只读 / 只写)
┌─────┴─────┐ ┌─────┴─────┐
ifstream istring- ofstream ostring-
(读文件) stream (写文件) stream
(读串) (写串)
└─────┬─────┘ └─────┬─────┘
└──── iostream ──────┘ ← 由 istream 与 ostream 双继承而来
│
┌─────────┴─────────┐
fstream stringstream ← 又能读又能写
(读写文件) (读写串)
从上往下看,记住四件事:
① 树根:ios —— 所有流的公共祖先
"状态检查"和"格式控制"是在这一层定义的,所以任何流都能用:
in.good(); in.fail(); in.eof(); // 状态检查 —— 15.4.1 细讲
cout.precision(2); cout.width(8); // 格式控制 —— 2.5.3 用过
这一条最实用:你只需要学一次 fail(),cin、ifstream、istringstream 上全都一样用。
② 第二层:istream / ostream —— 方向在此分家
istream家族:只能读。提供>>、get()、getline()、read()、seekg()/tellg()。ostream家族:只能写。提供<<、put()、write()、seekp()/tellp()、flush()。
"读"和"写"的能力在这一层就分开了——所以你不可能对一个 ifstream 写 <<(编译不过),这是编译器帮你挡住的错误,比运行期出错好得多。
③ 第三层:具体干活的三对类
| 你要读/写的地方 | 只读 | 只写 | 读写 |
|---|---|---|---|
| 磁盘文件 | ifstream |
ofstream |
fstream |
| 内存字符串 | istringstream |
ostringstream |
stringstream |
| 键盘 / 屏幕 | istream(cin) |
ostream(cout) |
— |
看这张表能悟出一件事:"文件"和"字符串"用的是同一套命名法 ——i/o 前缀 + 目标词 + stream。
这不是巧合:设计者故意让它们长得像,因为它们在代码里就是可以互相替换的(只要在 istream&/ostream& 这一层使用)。
④ 第四层:iostream —— 双继承的产物
fstream 和 stringstream 需要同时又读又写,所以标准库造了一个 iostream 类:
iostream由istream和ostream两个类多重继承而来。
这是 11.3「多重继承」在标准库里的真实应用——教材里那个"看起来没什么用"的语法,在整个 C++ 标准库里服务了几十年。
三、藏在这棵树里的一个秘密:这些类其实是模板
严格说,你在代码里写的 ifstream 不是一个真正的类名,它是:
typedef basic_ifstream<char> ifstream; // 我们在用的版本
// basic_ifstream<wchar_t> 是另一个版本(处理宽字符)
12.2 的类模板在这里藏得很深:整个流家族对 char 和 wchar_t 各有一份,靠模板做出来的(所以标准库不用把每个类写两遍)。
对你的影响:日常写 ifstream 就够了;只是当你在报错信息里看到 std::basic_ifstream<char> 这种长名字时,别慌,那就是 ifstream 的全名。
四、为什么要这么设计:把"通道"和"内容"拆开
流类库的设计里,有两件互相独立的事:
| 维度 | 管什么 | 换它意味着 |
|---|---|---|
| 通道(用哪个流对象) | 数据从哪儿来 / 到哪儿去 | 屏幕 ←→ 文件 ←→ 字符串 |
| 内容(用哪个成员函数/运算符) | 数据怎么解析 / 怎么格式化 | >> / getline / get / 格式控制 |
关键:这两件事是正交的(可以自由组合)。
int x;
cin >> x; // 键盘 + 按空白解析
in >> x; // 文件 + 按空白解析(同一套解析规则)
换个通道,解析规则不变;换个解析方式,通道不变。
这就是 15.1 说的"接口统一"在类结构上的落地——统一,靠的是继承:
只要函数/运算符写在父类上,所有子类自动获得。
五、动手 Demo:同一个函数,喂三种流
#include <iostream>
#include <fstream>
#include <sstream> // ← 字符串流在这里
#include <string>
using namespace std;
// 只认 ostream,完全不管对方是谁
void dump(ostream &out, const string &tag) {
out << "[" << tag << "] " << 3.14159 << endl;
}
int main() {
dump(cout, "screen"); // ① 屏幕
ofstream file("lib_demo.txt"); // ② 文件
if (!file) {
cout << "打不开文件" << endl;
return 1;
}
dump(file, "file");
file.close();
ostringstream buf; // ③ 内存里的字符串流
dump(buf, "memory");
string s = buf.str(); // 取出攒好的那串字
cout << "缓冲里拿到: " << s;
// 验证文件真的写进去了
ifstream in("lib_demo.txt");
string line;
getline(in, line);
cout << "文件里读到: " << line << endl;
in.close();
return 0;
}
看到什么算成功:
[screen] 3.14159
缓冲里拿到: [memory] 3.14159
文件里读到: [file] 3.14159
同时文件夹里多出 lib_demo.txt,内容是:
[file] 3.14159
读结果:
- 一个
dump函数,在三个不同的地方干活(屏幕 / 文件 / 内存),代码一个字没改 ——因为三者的共同父类都是ostream。 buf.str()只有ostringstream有,父类ostream没有。想用子类特有的功能,就得写出子类的名字 ——这正好印证了 11.1.1 的"吸收与不继承"。- 注意输出的顺序:
[screen]先出现,然后是"缓冲里拿到",最后是"文件里读到"。因为buf把内容攒在内存里,是后来cout打出来的。
六、30 秒实验
- 把
#include <sstream>删掉 →ostringstream报was not declared。四个头文件各管一段,缺一不可。 - 把
dump的形参类型改成ofstream &out→dump(cout, ...)和dump(buf, ...)同时报错。父子关系是单向的。 - 删掉
file.close();,直接跑 → 这次能读到内容(因为程序结束时析构自动刷新)。但你不能指望这个 ——15.5 会讲为什么"该关就关"。
七、常见误区
- "
ifstream是istream的子类,所以istream&能用的它都能用" ——方向对了:父类有的子类全有。但反过来不成立:istream&拿不到open()/close()(那是ifstream自己加的)。 - "
fstream是ifstream和ofstream的父类" ——正好反了。fstream是它俩的"兄弟":三者都从istream/ostream派生,fstream靠"双继承"同时拿到两边能力。 - "
<iostream>里已经有文件流了" ——没有。cout能用不代表ifstream能用,必须#include <fstream>。 - "字符串流是玩具,学习用的" ——它是最常用的文本工具之一:把一行
"Ada,95"拆成两段、把数字拼成字符串、清洗空格,都靠stringstream(13.4.2 里的istringstream就是这个家族的人)。 - "这棵树很重要,我要背下来" ——不用背。只要记住三层:
ios管公共规则 →istream/ostream管方向 → 具体类管去哪儿。真要查的时候,看一眼类名就知道它属于哪一支。
自测三问(点开对答案)
1. **只读文件、只写文件、读写文件,分别用哪个类?它们各属于 `istream` 还是 `ostream` 支?** ——`ifstream`(属 `istream` 支)/ `ofstream`(属 `ostream` 支)/ `fstream`(**双支**,因为它是 `iostream` 的子类)。**进错支的代价是编译报错**——比如对 `ifstream` 写 `<<` 编不过。 2. **为什么 `good()` / `fail()` / `eof()` 在 `cin`、`ifstream`、`istringstream` 上都能用?** ——**因为它们都在树根 `ios` 这一层定义**,**子类自动继承**。同理,`precision()` / `width()` 这些格式控制也在这一层。 3. **`iostream` 这个类是怎么来的?它解决了什么问题?** ——**由 `istream` 和 `ostream` 多重继承而来**(11.3)。**它让一个对象同时具备"读"和"写"两套能力**,`fstream` 和 `stringstream` 就靠它实现"可读可写"。- ☐ 下一节(15.1.2 类库的使用)
相关笔记
- 流简介 —— 上一节:流是什么、有哪几种
- 类库的使用 —— 下一节:真正把它用起来
- iostream 标准库 —— 最早认识的那个头文件
- 多重继承 ——
iostream的来历(教材外的真实应用) - 类模板 ——
ifstream背后的basic_ifstream<char> - 复制构造函数 —— 流对象不可复制
- 🔷 C++基础 · 索引 —— 全课路线图