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

文件读写只有两条路: - 文本文件:用 << >> / 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) 读 读一个字符(连空白也读)

文本路的核心特征:它关心"格式"。所以:

"格式"既是文本文件的优点(人能读),也是它的负担(读的时候要小心对齐)。

三、二进制路:两个函数

函数 干什么 签名
out.write(p, n) 从地址 p 开始,原样写 n 个字节 write(const char *s, streamsize n)
in.read(p, n) 往地址 p 写进去 n 个字节 read(char *s, streamsize n)

两个必须记住的点:

  1. 参数是 char * ——所以任何类型都得强转成 (char *)(它是"按字节搬运",不认类型);
  2. 必须配 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 的重点在"去文件夹里看那两个文件"):

顺手回答一个疑问:为什么二进制里那个字符串要"先写长度、再写内容"?

因为字符串的长度是变的——读的时候得先知道"要读几个字节"。这就是二进制文件的设计核心:双方必须对"布局"有完全一致的约定(15.3.4 细讲)。

六、30 秒实验

  1. 把 data_bin.dat 用记事本打开 → 看到乱码。再用 7-Zip 或十六进制查看器打开 → 能看清那些字节。你会看到 1001 变成了一串非文本字节(因为 int 是按二进制存的)。
  2. 把 ② 里的 ios::binary 删掉,再跑一次 → 在这个例子上,输出可能看不出差别(因为写的数据里没有 \n)。但把 name 换成一个含 \n 的字符串再试 → 文件大小会多 1 字节。这就是文本模式的换行符转换(15.3.4 会细讲)。
  3. 把①的 << 写法换成 write 试试 → 写出来的还是那段的字节,但少了"空格分隔",读的时候就没法再用 >> 切词了。体会"文本路靠分隔符,二进制路靠长度"。

七、常见误区

  1. "二进制文件就是'把内容加密了'" ——不是加密,是"没有转成文字"。加密是另一回事(本章不涉及)。
  2. "二进制文件一定比文本小" ——通常小,但不绝对。比如存很多 0/1 这种小数字,文本反而更省(1 个字符 vs 4 个字节)。
  3. "文本文件更安全" ——更"好读",但不是更安全。能随手改,也意味着容易被人改坏。
  4. "两条路可以随便混用" ——同一个文件里混着写会很难读回来。除非你设计得很清楚,否则一个文件只走一条路。
  5. "如果我只写 int,那文本和二进制差不多" ——差多了:文本写 100000 是 6 个字符,二进制写是 4 个字节,而且读的时候文本要用 >> 解析、二进制直接 read。
自测三问(点开对答案) 1. **文本文件和二进制文件最本质的区别是什么?** ——**文本文件里存的是"转成文字的表示"**(人能直接读,数字 `123` 变成三个字符);**二进制文件里存的是"内存里的原始字节"**(不转换,所以快、小,但只有程序能懂)。 2. **两条路各用什么函数读写?** ——**文本**:写 `<<` / `put`,读 `>>` / `getline` / `get`;**二进制**:写 `write`,读 `read`(**都要配 `ios::binary`**)。 3. **二进制的 `write` / `read` 为什么参数是 `char *`?** ——**因为它做的是"按字节搬运"**,不认识任何类型。**所以任何数据都要 `(char *)&变量` 强转过去,并配 `sizeof` 说明要搬几个字节。**

相关笔记