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

ios 类里定义了一堆枚举常量,它们是流世界的"开关词汇表"。三大类: - ios::in / out / app / ate / trunc / binary ——怎么打开(打开方式); - ios::beg / cur / end ——从哪儿开始数(定位基准); - ios::dec / hex / fixed / left … ——怎么写好看(格式标志)。 用法的关键在一句话:这些常量是"2 的幂"的标志位,可以拿 | 拼起来 ——所以"追加着写、还是二进制"这种组合意图,一个参数就能表达。 一句话概括:这些常量不是新语法,是"把一串意图打包成一个整数"的标准做法(8.6.1 讲过的老手艺)。

📝 本节和前面几节的关系

本节是补工具,为 15.2 起的所有打开操作做铺垫: - 回看 8.6.1:那里讲过"用 2 的幂做可叠加的标志位"(OK=1, CREATED=2, ACCEPTED=4)。打开方式用的就是同一个套路——in、out、app 各占一个二进制位,所以能 |。 - 回看 11.2:| 能在这儿使,是因为标准库重载了 operator|(11.2.2 讲的"运算符就是函数")。 - 回看 2.5.3:那里用过的 setprecision / setw 是 <iomanip> 的操纵符,它们底层改的就是这里第三组"格式标志"。 一句话:本节把"打开文件时那几个看不懂的词"一次性讲清。

一、三组常量,一张总表

它们都挂在 ios 类里(15.1.1 讲过:ios 是所有流的公共祖先),所以用的时候要带上 ios:: 这个前缀。

组 代表常量 管什么 用在哪
打开方式 in out app ate trunc binary 文件怎么开 open() / 构造函数
定位基准 beg cur end seekg/seekp 从哪儿数 15.4.4
格式标志 dec hex oct left right fixed scientific boolalpha showpoint … 数据怎么写出来 setf() 或 <iomanip>

为什么前缀是 ios:: 而不是 ifstream::?

因为它们在树根那一层定义 ——所有流共享。所以 ios::in 写在 ifstream 的参数里,也写在 fstream 的参数里,不用换前缀。

二、第一组:打开方式(openmode)—— 本章最常用

常量 含义 记法
ios::in 为读取而打开 in = 进
ios::out 为写入而打开 out = 出
ios::app 追加(append):每次写都写到文件末尾 app = append
ios::ate 打开后立刻把位置移到末尾(at end),之后还可以自由挪 ate = at end
ios::trunc 截断(truncate):文件存在就把内容清空 trunc = truncate
ios::binary 二进制模式(不做换行符转换) 15.3.4 细讲

怎么用:用 | 把需要的拼起来(或者更好写:|=)。

ifstream in("a.txt",  ios::in);                        // 只读
ofstream out("a.txt", ios::out);                       // 只写(会清空)
ofstream log("a.txt", ios::out | ios::app);            // 追加写
fstream  db("a.txt",  ios::in  | ios::out);            // 可读可写、不清空
ifstream bin("a.dat", ios::in  | ios::binary);         // 二进制读
⚠️ 本章第一号事故:`ofstream` 打开已有文件会清空它
ofstream out("important.txt");     // 只要这一行跑过去,文件就空了

因为 ofstream 的默认模式里含 trunc(15.2.2 会列出来)。 想"接着写"必须显式加 app: ofstream out("important.txt", ios::app); 这一条每年都会让无数人丢掉数据。 记住它。

三、第二组:定位基准(seekdir)—— 15.4 用

常量 值 意思
ios::beg 0 从文件开头算(beginning)
ios::cur 1 从当前位置算(current)
ios::end 2 从文件末尾算(end)

典型用法(记住这两个就够):

in.seekg(0, ios::beg);          // 回到开头(等价于 in.seekg(0);)
in.seekg(0, ios::end);          // 挪到末尾
long size = in.tellg();         // 读位置 = 文件总字节数

"从末尾往前数"要用负数:in.seekg(-5, ios::end); 表示"末尾往前 5 个字节"。

四、第三组:格式标志(fmtflags)—— 和 2.5.3 接上

这一组管的是"数字怎么写出来"。日常你更常用 <iomanip> 里的操纵符,但两者是一回事:

标志 效果 <iomanip> 里对应的操纵符
ios::dec / hex / oct 十进制 / 十六进制 / 八进制 dec / hex / oct
ios::fixed 定点小数 fixed
ios::scientific 科学计数法 scientific
ios::left / right 左 / 右对齐 left / right
ios::showpoint 强制显示小数点 showpoint
ios::boolalpha bool 打 true/false boolalpha
ios::skipws 跳过输入前导空白(默认开) skipws
ios::showbase 十六进制带 0x 前缀 showbase

两种改法:

// 写法一:用 setf() 直接改标志位(老教材风格)
cout.setf(ios::hex, ios::basefield);       // 切到十六进制
cout.setf(ios::showbase);                  // 加 0x 前缀
cout << 255 << endl;                       // 0xff

// 写法二:用操纵符(更常见、更短)
#include <iomanip>
cout << hex << showbase << 255 << endl;     // 0xff

为什么 setf 那行要写两个参数? 因为 hex/dec/oct 是一组互斥的选项,要先把这一组清干净再设新值(只写一个参数就是"叠加",会得到奇怪的组合)。这也是为什么"用操纵符更省心"。

对你的实际影响:日常用操纵符;看到 setf 认得就行。

五、为什么能"拼起来":位标志位的原理

这是 8.6.1 那个老手艺的原样重演。

六个打开方式,各占一个二进制位(值不重要,只要不重叠):

ios::in      = 0000 1000
ios::out     = 0001 0000
ios::trunc   = 0010 0000
ios::app     = 0100 0000
ios::binary  = 1000 0000

ios::in | ios::binary  = 1000 1000    ← 两个位同时为 1:意思是"两个都要"

所以 | 不是"加减",是"把两个指令同时挂上"。

顺带解释一件事:ios::in | ios::out 这种写法能编译过,是因为标准库为这些标志位类型重载了 operator| 和 operator|=(11.2)。如果它是普通的 enum,| 会报类型错 ——这也是为什么较新的代码里你会看到 ios::openmode(ios::in | ios::binary) 这种别扭的强转(C++ 老枚举的问题,8.6.1 提过 enum class 更安全,但会丢掉这种"自动当整数用"的方便)。

六、动手 Demo:把三组常量都摸一遍

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

int main() {
    // ── ① 打开方式:先用 out|trunc 清空写,再用 app 追加 ──
    ofstream o1("modes.txt", ios::out | ios::trunc);   // 清空重写
    if (!o1) { cerr << "打开失败" << endl; return 1; }
    o1 << "first run" << endl;
    o1.close();

    ofstream o2("modes.txt", ios::out | ios::app);     // 追加
    if (!o2) { cerr << "打开失败" << endl; return 1; }
    o2 << "second run" << endl;
    o2.close();

    // ── ② 定位基准:量一下文件多大 ──
    ifstream in("modes.txt", ios::in);
    in.seekg(0, ios::end);                             // 挪到末尾
    long size = in.tellg();                            // 读位置 = 大小
    cout << "② 文件大小: " << size << " 字节" << endl;

    in.seekg(0, ios::beg);                             // 回到开头
    cout << "② 内容:" << endl;
    string line;
    while (getline(in, line)) cout << "   " << line << endl;
    in.close();

    // ── ③ 格式标志:同一个数,三种写法 ──
    cout << "③ 默认   : " << 255 << endl;
    cout << "③ 十六进制: " << hex << showbase << 255 << dec << endl;
    cout << "③ 八进制 : " << oct << 255 << dec << endl;
    return 0;
}

看到什么算成功:

② 文件大小: 21 字节
② 内容:
   first run
   second run
③ 默认   : 255
③ 十六进制: 0xff
③ 八进制 : 377

数字怎么来的(都验算过):

读结果:

七、30 秒实验

  1. 把 ① 里的 ios::app 改成 ios::out(去掉 app),连跑两次程序 → 第二次跑完,文件里只剩 first run。 亲眼看到 trunc 的威力。(这也是本章最需要"亲眼见一次"的坑。)
  2. 把 in.seekg(0, ios::beg); 那一行注释掉 → 内容一行都读不出来(因为读位置还停在末尾)。证明"读位置"是真的存在、真的会卡人。
  3. 把 ③ 第一行末尾也加上 dec,再跑 → 三个数字都是十进制。证明格式标志是"粘性"的。

八、常见误区

  1. "常量名前缀该写 ifstream::" ——写 ios::。它们在树根定义,所有流共享(15.1.1)。ios::app 在 ofstream 和 fstream 上都通用。
  2. "ios::app 和 ios::ate 差不多" ——差得挺远:app 是"以后每次写都强制到末尾";ate 只是"打开时先挪到末尾一次",之后你想 seek 到别处照样能写。(15.4.2 会专门对照)
  3. "打开方式只能写一个" ——可以 | 组合,因为它们是比特标志位(8.6.1 的套路)。
  4. "ios::out 打开文件不会清空" ——会清(因为 ofstream 的默认模式含 trunc)。要"只写不清"得显式管好模式。
  5. "格式标志设一次就行了" ——它是粘性的(sticky):一旦设了 hex,后面所有数字都是十六进制,记得切回 dec。
  6. "这些常量就是普通的 int,能随便加" ——概念上不是整数,是"比特标志位"。用 | 不用 +(+ 遇到相同位会进位,把两个标志变成第三个意思)。
自测三问(点开对答案) 1. **`ios::app` 和 `ios::ate` 分别是什么意思?** ——**`app`(append)**:**每次写入都到文件末尾**;**`ate`(at end)**:**打开时把位置移到末尾一次,之后可以自由挪**。**想做日志就 `app`;想"从末尾开始但还能回头改"就 `ate`。** 2. **为什么这些常量能用 `|` 拼起来?** ——**它们是 2 的幂(各占一个二进制位)的标志位**(8.6.1 的老手艺),**`|` 是把多个标志"同时挂上"**;而且**标准库重载了 `operator|`**,所以 `ios::in | ios::binary` 才能编过(11.2)。 3. **`cout << 255` 之前写了 `cout << hex;`,之后所有输出都会变成十六进制吗?** ——**会。格式标志是粘性的**,**一直有效到你再改它**。**所以用完记得 `dec` 切回来。**

相关笔记