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

写文本文件就是把 cout 换成文件流对象——<< 的用法一模一样,连 <iomanip> 那套格式控制都照用。 两个本节才需要注意的点: - << 会自动把各种类型转成文字(int / double / string 都能直接写); - endl 会刷盘、"\n" 不会 ——写大量数据时差别明显。 一句话概括:写文件是"输出"的搬家——语法不变,只是换了个出口。

📝 本节和前面几节的关系

15.3.1 打通了读写闭环,本节只管"写"这一半,讲透它: - 2.5.3 的格式控制(setw / setprecision / fixed)在这里第二次上岗——那时写给屏幕,现在写给文件,一样有效; - 15.2.2 的"ofstream 默认清空"在这里第一次变成实际影响(覆盖 vs 追加); - 8.4 的结构体数组 在这里被"导出"到文件(实例04 就是导出一张成绩表)。 一句话:本节是"输出"这一侧的完整交代。

一、<< 能直接写各种类型

写文本文件时,<< 会先把数据"转成文字"再写出去:

ofstream out("a.txt");
int    n = 42;
double d = 3.14;
string s = "hello";
char   c = 'X';
bool   b = true;

out << n << " " << d << " " << s << " " << c << " " << b << endl;
out.close();

文件内容(用记事本打开):

42 3.14 hello X 1

注意最后那个 1 ——bool 默认打成 1/0。想让它打 true/false,要加 boolalpha(15.1.3 的格式标志表里有它):

out << boolalpha << b << endl;         // true

这就是"文本文件"的实质:任何类型都变成一串字符。"42" 是两个字符,不是那个 4 字节的 int。

二、格式控制:写得更"像样子"

<iomanip> 里的东西写文件时照样管用,因为它们改的是流本身的格式标志(15.1.3 讲过第三组常量)。

#include <iomanip>

out << left  << setw(10) << "name" << setw(8) << "score" << endl;   // 表头左对齐
out << fixed << setprecision(2) << 1.6 << endl;                     // 1.60(两位小数)
out << setfill('.') << setw(8) << 42 << endl;                       // .......42

三条必记的规矩(2.5.3 都讲过,这里复习):

操纵符 生效范围 说明
setw(n) 只对"下一个"输出生效 每个值前面都要重写
setprecision(n) / fixed / left / right / setfill 粘性,一直有效 改了就一直用,直到你改回来
left / right 粘性 setw 决定多宽,left/right 决定靠哪边

关于对齐的一个真相:表头和数据的"看起来齐不齐",取决于"列宽"和"对齐方式"是不是为它们分别设计的。 比如表头写 score(5 个字符)右对齐到 6 位,数据 95(2 个字符)也右对齐到 6 位 ——右端是对齐的,但左端差很远。 所以"做表格"最省心的做法是:所有列统一左对齐、列宽给够。

三、endl 和 "\n" 的差别(写文件时很重要)

写法 做什么 代价
out << "\n"; 只放一个换行符进缓冲区 小
out << endl; 换行 + 立刻刷缓冲区(flush) 大(每次都要真正访问磁盘)

写几行看不出差别,写十万行就是天壤之别:

那什么时候必须用 endl?

需要"立刻让人/别的程序看到"的时候。 比如写日志——程序正在跑,你希望日志文件随时是最新的,那就用 endl(或者偶尔 out.flush())。 纯粹攒数据(导出报表、批量写记录)→ 用 "\n"。

另外注意一个平台差异:Windows 上文本模式写 \n,磁盘上实际存的是 \r\n(两个字节) ——这是文本模式的自动转换(15.3.4 详讲)。所以你数文件大小时,别按"字符个数"算。

四、实例04:向文本文件中写入数据

教材这道题要做的:把一组数据(成绩表)写进文本文件,并演示覆盖与追加两种方式。

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

struct Student { string name; int score; double height; };

int main() {
    Student cls[3] = {
        {"Ada", 95, 1.62},
        {"Bob", 88, 1.78},
        {"Cid", 72, 1.71}
    };

    // ══ ① 朴素写法:直接用空格分隔(最容易再读回来)══
    {
        ofstream out("stu_plain.txt");
        if (!out) { cerr << "写不了 stu_plain.txt" << endl; return 1; }
        for (int i = 0; i < 3; i++)
            out << cls[i].name << " " << cls[i].score << " " << cls[i].height << "\n";
        out.close();
    }

    // ══ ② 带格式:排成一张能看的表 ══════════════
    {
        ofstream out("stu_table.txt");
        if (!out) { cerr << "写不了 stu_table.txt" << endl; return 1; }
        out << left << setw(10) << "name" << setw(8) << "score" << "height" << "\n";
        out << string(24, '-') << "\n";
        for (int i = 0; i < 3; i++) {
            out << left << setw(10) << cls[i].name
                << setw(8) << cls[i].score
                << fixed << setprecision(2) << cls[i].height << "\n";
        }
        out.close();
    }

    // ══ ③ 追加一行(不清空已有内容)════════════
    {
        ofstream out("stu_plain.txt", ios::app);       // ★ app 是关键
        if (!out) { cerr << "追加失败" << endl; return 1; }
        out << "Dee 60 1.68" << "\n";
        out.close();
        cout << "③ 已向 stu_plain.txt 追加一行 Dee" << endl;
    }

    // ══ ④ 把两个文件都打出来看看 ═══════════════
    const char *files[2] = {"stu_plain.txt", "stu_table.txt"};
    for (int f = 0; f < 2; f++) {
        cout << "── " << files[f] << " ──" << endl;
        ifstream in(files[f]);
        string line;
        while (getline(in, line)) cout << line << endl;
        in.close();
    }
    return 0;
}

看到什么算成功:

③ 已向 stu_plain.txt 追加一行 Dee
── stu_plain.txt ──
Ada 95 1.62
Bob 88 1.78
Cid 72 1.71
Dee 60 1.68
── stu_table.txt ──
name      score   height
------------------------
Ada       95      1.62
Bob       88      1.78
Cid       72      1.71

数字怎么来的:分隔线是 string(24, '-'),24 = 10 + 8 + 6(三列的宽度加起来,最后一列是 "height" 的 6 个字符)。

读结果:

  1. ① 和 ② 的内容完全一样,只是"排版不同" ——数据还是那些数据。这说明格式控制只是"好看",不影响内容。
  2. ③ 说明 ios::app 真的没清空:stu_plain.txt 里前 3 行还在,Dee 接在第 4 行。如果把 ios::app 删掉,再跑一次 → 前 3 行会被清掉、只剩 Dee(15.2.2 的默认 trunc)。
  3. ① 用的是 "\n"、③ 用的是 "\n" ——没别的意思,就是想让你看到"不写 endl 也能换行"。

两道顺手的练习题:

五、30 秒实验

  1. 把 ③ 的 ios::app 删掉,连跑两次 → 第一次跑完有 4 行,第二次跑完只剩 1 行(Dee)。这就是"覆盖 vs 追加"的现场对照。
  2. 在 ① 的循环里把 "\n" 改成 endl,把 3 改成 300000(三十万行),掐表比较 → endl 明显更慢。 这就是"刷盘"的代价。
  3. 在 ② 里把 left 删掉 → 表头和数据都变成右对齐,看起来更"散"。证明 left/right 是粘性的、影响所有后续输出。
  4. 把 ① 写出的 stu_plain.txt 用 Excel 打开 → 一列里全是 Ada 95 1.62(因为它不认识空格分隔)。把分隔符换成逗号再试 → 自动分成三列。这就是 CSV 的由来。

六、常见误区

  1. "写文件要用 write,<< 是给屏幕用的" ——<< 写文本文件完全正确。write 是二进制路(15.3.4)。
  2. "每行结尾必须写 endl" ——不是必须的。"\n" 一样换行,而且快得多。只在需要"立刻落地"时才用 endl。
  3. "bool 写进文件是 true/false" ——默认是 1/0,要 boolalpha 才是文字。
  4. "格式控制是 cout 专有的" ——不是,<iomanip> 对任何 ostream 都有效,包括文件流和字符串流。
  5. "setw(10) 设一次,后面都管用" ——setw 只对下一个输出生效(它是唯一"非粘性"的)。其他格式标志都是粘性的。
  6. "用 << 写文件不用担心换行符" ——在文本模式下不用担心(系统会转换),但在二进制模式下不会转换,这一点在 15.3.4 会变成关键差别。
自测三问(点开对答案) 1. **写文本文件时,`endl` 和 `"\n"` 有什么差别?什么时候该用哪个?** ——**`endl` 是"换行 + 立刻刷缓冲区"**(每次写盘),**`"\n"` 只是放个换行符**。**要实时可见(如日志)用 `endl`;批量写数据用 `"\n"`(快得多)。** 2. **`setw` 和其他格式操纵符(`fixed` / `setprecision` / `left`)的生效范围有何不同?** ——**`setw` 只对"下一个"输出生效,每次都要重写**;**其他几个是粘性的**,设了就一直有效,直到被改。 3. **把一个成绩表"追加"到一个已有文本文件里,关键要写什么?不写会怎样?** ——**关键是在打开时写 `ios::app`**。**不写的话用的是 `ofstream` 的默认模式(含 `trunc`),会把原文件清空、只剩新写的内容。**

相关笔记