文件读写只有两条路:
- 文本文件:用 << >> / getline / get / put ——数据被转成"人能看懂的文字";
- 二进制文件:用 write / read ——内存里的字节原样搬进搬出。
判据一句话:"这个文件最终是给人看的,还是给程序读的?" ——给人看用文本,给程序用二进制。
两条路的接口完全不同,但底层是同一个流——所以"打开、检查、关闭"那套流程两边通用。
15.1 讲流、15.2 讲打开,本节开始"真正的内容交换":
- 本章接下来五节就是这条分岔路的展开:15.3.1 总起 → 15.3.2 / 15.3.3 走文本 → 15.3.4 走二进制 → 15.3.5 用二进制做文件复制;
- 13.4.1 的 ostream_iterator / back_inserter 在这里能派上用场(把容器直接写进文件);
- 8.1.5 的"结构体大小 ≠ 成员相加"在 15.3.4 会变成真问题。
一句话:本节是"分岔口的路牌",看清两条路的性格,再决定走哪条。
一、两条路,两种性格
| 文本文件 | 二进制文件 | |
|---|---|---|
| 内容长什么样 | 人能直接看懂(打开记事本能读) | 一堆字节(打开多半是乱码) |
| 用什么读 | >> / getline / get |
read |
| 用什么写 | << / put |
write |
| 数字怎么存 | 转成字符(123 → 三个字符 '1' '2' '3') |
内存里什么样就什么样(一个 int 4 字节) |
| 占空间 | 大(4 字节的 int 可能写成 10 个字符) | 小(4 字节就是 4 字节) |
| 速度 | 慢(要做"数字 ↔ 文字"的转换) | 快(直接搬字节) |
| 跨平台 | 好(换行符/编码由系统处理) | 差(字节序、类型长度、对齐都可能不一样) |
| 能不能手工改 | 能(记事本就能改) | 基本不能(改了可能整个文件废掉) |
| 适合 | 配置、日志、CSV、成绩单 | 图片、音频、可执行文件、定长记录 |
一句话判据:
需要人看 / 需要跨平台 / 需要手工编辑 → 文本; 只要程序自己用 / 追求速度和体积 → 二进制。
二、文本路:五个工具
| 工具 | 读/写 | 干什么 |
|---|---|---|
out << x |
写 | 按类型转成文字写出去(int、double、string 都行) |
out.put(ch) |
写 | 写一个字符(<< 也能写字符,put 更明确) |
in >> x |
读 | 跳过空白、按类型解析一个值 |
getline(in, line) |
读 | 读一整行(含空格,不含换行符) |
in.get(ch) |
读 | 读一个字符(连空白也读) |
文本路的核心特征:它关心"格式"。所以:
- 写的时候可以用
<iomanip>控制格式(setprecision/setw/fixed); - 读的时候必须知道写入时的格式,否则解析会错(比如你写的是
3.14,却用in >> n(int)去读,只会拿到3)。
"格式"既是文本文件的优点(人能读),也是它的负担(读的时候要小心对齐)。
三、二进制路:两个函数
| 函数 | 干什么 | 签名 |
|---|---|---|
out.write(p, n) |
从地址 p 开始,原样写 n 个字节 |
write(const char *s, streamsize n) |
in.read(p, n) |
往地址 p 写进去 n 个字节 |
read(char *s, streamsize n) |
两个必须记住的点:
- 参数是
char *——所以任何类型都得强转成(char *)(它是"按字节搬运",不认类型); - 必须配
ios::binary打开 ——否则 Windows 上换行符会被偷偷转换。
最基本的用法:
int n = 12345;
out.write((char *)&n, sizeof(n)); // 写 4 个字节
int m;
in.read((char *)&m, sizeof(m)); // 读回来正好也是原来的值
注意 &:写的是"变量在内存里的那一段字节"。这就是"二进制文件"最本质的一句话:它保存的是内存的复本。
四、两条路怎么选:三个具体场景
| 场景 | 选哪条 | 为什么 |
|---|---|---|
| 写一份"学生成绩表",要能被 Excel 打开 | 文本(CSV 格式) | 要给人看、要跨工具 |
| 保存游戏存档 / 程序的状态快照 | 二进制 | 只给程序读、要快、要精确 |
| 写运行日志 | 文本 | 要能直接翻、直接搜 |
| 复制一张图片 / 一个 exe | 二进制 | 内容本来就是字节,文本模式会破坏它(15.3.5) |
| 保存一堆"结构完全一样的记录" | 二进制(定长记录,15.4.4) | 每条记录长度固定 → 能直接跳到第 N 条 |
五、动手 Demo:同一个数据,两条路各写一份
目的:亲眼看见"同一份数据在两种文件里长得完全不一样"。
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
int id = 1001;
double score = 95.5;
string name = "Ada";
// ── ① 文本方式写 ───────────────────────
{
ofstream t("data_text.txt");
if (!t) { cerr << "写不了文本文件" << endl; return 1; }
t << id << " " << name << " " << score << endl;
t.close();
}
// ── ② 二进制方式写 ─────────────────────
{
ofstream b("data_bin.dat", ios::binary);
if (!b) { cerr << "写不了二进制文件" << endl; return 1; }
b.write((char *)&id, sizeof(id));
b.write((char *)&score, sizeof(score));
// 字符串要特殊处理:先写长度、再写内容(原因见 15.3.4)
int len = (int)name.size();
b.write((char *)&len, sizeof(len));
b.write(name.c_str(), len);
b.close();
}
// ── ③ 各读回来看看 ─────────────────────
{
ifstream t("data_text.txt");
string line;
getline(t, line);
cout << "① 文本文件读回来: [" << line << "]" << endl;
t.close();
}
{
ifstream b("data_bin.dat", ios::binary);
int rid = 0;
double rscore = 0;
int rlen = 0;
b.read((char *)&rid, sizeof(rid));
b.read((char *)&rscore, sizeof(rscore));
b.read((char *)&rlen, sizeof(rlen));
string rname(rlen, '\0'); // 先造出 rlen 个字符
b.read(&rname[0], rlen); // 再读进去
cout << "② 二进制读回来 : id=" << rid
<< " score=" << rscore
<< " name=" << rname << endl;
b.close();
}
cout << "③ 去看这两个文件:文本那个记事本能读懂,二进制那个多半是乱码" << endl;
return 0;
}
看到什么算成功:
① 文本文件读回来: [1001 Ada 95.5]
② 二进制读回来 : id=1001 score=95.5 name=Ada
③ 去看这两个文件:文本那个记事本能读懂,二进制那个多半是乱码
读结果(本 Demo 的重点在"去文件夹里看那两个文件"):
data_text.txt:打开记事本,内容是1001 Ada 95.5——人能直接读。data_bin.dat:打开多半是乱码,而且大小是 4 + 8 + 4 + 3 = 19 字节(int4 +double8 + 长度 4 +"Ada"3)。这个小尺寸证明它没有做任何"转成文字"的加工。- 对比一下:文本那份是 14 个字符 = 14 字节(
10014 + 空格 1 +Ada3 + 空格 1 +95.54 + 换行 1 = 14)。
顺手回答一个疑问:为什么二进制里那个字符串要"先写长度、再写内容"?
因为字符串的长度是变的——读的时候得先知道"要读几个字节"。这就是二进制文件的设计核心:双方必须对"布局"有完全一致的约定(15.3.4 细讲)。
六、30 秒实验
- 把
data_bin.dat用记事本打开 → 看到乱码。再用 7-Zip 或十六进制查看器打开 → 能看清那些字节。你会看到1001变成了一串非文本字节(因为int是按二进制存的)。 - 把 ② 里的
ios::binary删掉,再跑一次 → 在这个例子上,输出可能看不出差别(因为写的数据里没有\n)。但把name换成一个含\n的字符串再试 → 文件大小会多 1 字节。这就是文本模式的换行符转换(15.3.4 会细讲)。 - 把①的
<<写法换成write试试 → 写出来的还是那段的字节,但少了"空格分隔",读的时候就没法再用>>切词了。体会"文本路靠分隔符,二进制路靠长度"。
七、常见误区
- "二进制文件就是'把内容加密了'" ——不是加密,是"没有转成文字"。加密是另一回事(本章不涉及)。
- "二进制文件一定比文本小" ——通常小,但不绝对。比如存很多
0/1这种小数字,文本反而更省(1 个字符 vs 4 个字节)。 - "文本文件更安全" ——更"好读",但不是更安全。能随手改,也意味着容易被人改坏。
- "两条路可以随便混用" ——同一个文件里混着写会很难读回来。除非你设计得很清楚,否则一个文件只走一条路。
- "如果我只写
int,那文本和二进制差不多" ——差多了:文本写100000是 6 个字符,二进制写是 4 个字节,而且读的时候文本要用>>解析、二进制直接read。
自测三问(点开对答案)
1. **文本文件和二进制文件最本质的区别是什么?** ——**文本文件里存的是"转成文字的表示"**(人能直接读,数字 `123` 变成三个字符);**二进制文件里存的是"内存里的原始字节"**(不转换,所以快、小,但只有程序能懂)。 2. **两条路各用什么函数读写?** ——**文本**:写 `<<` / `put`,读 `>>` / `getline` / `get`;**二进制**:写 `write`,读 `read`(**都要配 `ios::binary`**)。 3. **二进制的 `write` / `read` 为什么参数是 `char *`?** ——**因为它做的是"按字节搬运"**,不认识任何类型。**所以任何数据都要 `(char *)&变量` 强转过去,并配 `sizeof` 说明要搬几个字节。**- ☐ 下一节(15.3.1 文件流)