"追加"就是在文件末尾接着写,有两条路:
- 打开时用 ios::app ——一劳永逸:之后每次写都被强制拉到末尾(连 seekp 都拉不回来);
- 打开后用 seekp(0, ios::end) ——手动搬一次写位置,之后还能自由挪到别处写。
而它们的共同点、也是本节反复强调的:千万别忘了 app ——忘了它,ofstream 的默认 trunc 会把原文件清空。
一句话概括:"追加"不是"写到末尾"这么简单,它是"打开方式"和"写位置"两件事的配合。
本节把散在三处的知识缝在一起:
- 15.2.1 讲过 app vs ate 的差别;15.2.2 讲过 ofstream 默认清空;15.4 讲了写位置 ——本节是这三条的交汇点;
- 15.4.1 的失败状态在这里再次出现(在失败的流上写不会生效);
- ate 与 app 的分水岭在本节得到最清楚的实证。
一句话:本节是"写位置"这一侧最实用的一节——因为"追加"是真实工作里最常见的写文件方式(日志、记录、累积数据)。
一、三种打开方式,三种"写位置"命运
这是本节的核心对照表:
| 打开方式 | 打开后写位置在哪 | 之后能不能 seekp 到别处写 |
|---|---|---|
ios::out \| ios::trunc(ofstream 默认) |
开头 | 能(但旧内容已被清空) |
ios::out \| ios::app |
末尾 | 不能 ——每次写都被强制拉回末尾 |
ios::out \| ios::ate |
末尾 | 能 |
ios::in \| ios::out(fstream 默认) |
开头(不清空) | 能 |
从表里能读出三句关键结论:
app是"强制"的 ——它的语义是"以后所有写都追加",所以你手动seekp也无效;ate只是"开局站到末尾" ——之后再挪照样能写;in | out打开已有文件时,写位置在开头 ——直接写就会覆盖开头的内容(这是最容易被忽略的一条)。
二、两条实现"追加"的路
路一:打开时就用 app(推荐,最省心)
ofstream log("app.log", ios::app);
if (!log) { /* 处理失败 */ }
log << "一条新的记录" << endl; // 自动写到末尾
log.close();
优点:一句话表达清楚意图,再也不用管位置。
适合:日志、累积记录、追加导出 ——这也是 app 存在的意义。
路二:打开后手动 seekp 到末尾(更灵活)
fstream f("data.txt", ios::in | ios::out); // 可读可写,不清空
if (!f) { /* 处理失败 */ }
f.seekp(0, ios::end); // ★ 把写位置搬到末尾
f << "追加的内容" << endl;
f.close();
优点:搬完之后还能再 seekp 到别处(比如"追加完一条,再回头改前面的某个字段")。
缺点:多一步,容易忘 ——忘了 seekp 就从开头覆盖了。
两条路的取舍
ios::app |
手动 seekp(0, ios::end) |
|
|---|---|---|
| 写法 | 简单 | 多一行 |
| 意图清晰度 | 高(一眼看出是追加) | 中 |
| 之后能否改别处 | 不能 | 能 |
| 适合 | 纯追加(日志) | 追加 + 回头修改 |
三、为什么"日志文件"必须用 app
日志的三个特点,正好是 app 的三个卖点:
- 只往后加,从不回头改 → 正好符合
app的"强制末尾"; - 文件常常已经存在(昨天、前天的日志都还在)→
app不会清空(换默认ofstream就把历史日志全毁了); - 程序可能被强制结束 → 即使那样,已经写进去的部分也还在(
app每次都定位到末尾,不会覆盖)。
一句话:
写日志,永远
ofstream log("x.log", ios::app);这一行如果你写成了ofstream log("x.log");——跑第一次就把日志清空了。
四、动手 Demo:五种情形,一眼看清
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
void show(const string &tag, const string &f) {
ifstream in(f);
cout << tag << " [" << f << "] ";
string line;
while (getline(in, line)) cout << line << " / ";
cout << endl;
in.close();
}
int main() {
// ══ ① app:两次打开都在末尾追加 ══
{ ofstream o("m_app.txt", ios::app); o << "A" << endl; }
{ ofstream o("m_app.txt", ios::app); o << "B" << endl; }
show("① app 追加两次 :", "m_app.txt");
// ══ ② 默认 out|trunc:每次打开都清空 ══
{ ofstream o("m_out.txt", ios::out); o << "A" << endl; }
{ ofstream o("m_out.txt"); o << "B" << endl; } // ★ 默认含 trunc
show("② 默认 out 覆盖两次 :", "m_out.txt");
// ══ ③ in|out 直接写:从开头覆盖 ══
{ ofstream o("m_seek.txt"); o << "A" << endl; }
{
fstream f("m_seek.txt", ios::in | ios::out); // 不清空,但位置在开头
f << "X" << endl; // ★ 覆盖掉原来的 A
f.close();
}
show("③ in|out 直接写 :", "m_seek.txt");
// ══ ④ in|out + seekp 末尾:正确的手动追加 ══
{ ofstream o("m_seek2.txt"); o << "A" << endl; }
{
fstream f("m_seek2.txt", ios::in | ios::out);
f.seekp(0, ios::end); // ★ 先搬到末尾
f << "B" << endl;
f.close();
}
show("④ in|out + seekp 末尾:", "m_seek2.txt");
// ══ ⑤ app 模式下,seekp 也拉不回来 ══
{ ofstream o("m_app2.txt"); o << "AAAAA" << endl; }
{
ofstream o("m_app2.txt", ios::app);
o.seekp(0, ios::beg); // 想回到开头
o << "X" << endl; // ★ 实际还是写在末尾
o.close();
}
show("⑤ app 里 seekp 无效 :", "m_app2.txt");
return 0;
}
看到什么算成功:
① app 追加两次 : [m_app.txt] A / B /
② 默认 out 覆盖两次 : [m_out.txt] B /
③ in|out 直接写 : [m_seek.txt] X /
④ in|out + seekp 末尾: [m_seek2.txt] A / B /
⑤ app 里 seekp 无效 : [m_app2.txt] AAAAA / X /
读结果——五行,五个结论:
| 行 | 结论 |
|---|---|
| ① | app 两次打开都在末尾接(A、B 都在)——这就是追加 |
| ② | 默认 out 每次都清空(只剩 B)——trunc 的实锤(15.2.2) |
| ③ | in \| out 打开后写位置在开头 → X 覆盖了原来的 A ——这条最容易被忽略 |
| ④ | 加一句 seekp(0, ios::end) 就变成正确追加 ——这就是"手动追加" |
| ⑤ | 在 app 模式下 seekp(0, ios::beg) 完全无效,X 还是落在末尾 ——证明 app 是"强制末尾" |
这一张输出,就把"追加"的全部要点讲完了。
五、30 秒实验
- 把 ① 的
ios::app删掉(变成默认ofstream),再跑 → 只剩B。这就是"忘了写app的代价"。 - 在 ⑤ 里把
ios::app换成ios::ate,再跑 →X会覆盖开头(因为ate只负责"开局末尾",之后seekp(0)真的能回到开头)——结果变成XAAAA。这就是app与ate的分水岭。 - 把 ③ 改成
fstream f("m_seek.txt", ios::in | ios::out | ios::app);→X落在末尾,A保住了。这就是"既想读写、又想追加"的写法。
六、常见误区
- "想追加就写
ios::out" ——out会清空(ofstream默认含trunc)。追加必须app(或手动seekp到末尾)。 - "
app和ate都是末尾,写ate更灵活" ——看需求:纯追加用app(更安全,不会被误 seek 破坏);要"末尾 + 回头改"用ate。 - "用
in | out打开就能追加" ——不能。写位置在开头,直接写会覆盖。 要追加必须seekp(0, ios::end)。 - "
seekp之后就一直有效" ——在app模式下seekp对写操作无效(每次写前都会被拉回末尾)。要自由定位就别用app。 - "追加的内容会另起一行" ——不会。文件没有"行"的概念,只有字节。你上次写的内容如果没有以换行结尾,新内容会直接接在它后面(
AAAAA后面接X就是AAAAAX)。要换行就自己写\n或endl。 - "写完不关也没事,反正下次还会追加" ——有事:这次的数据可能还在缓冲区,而且文件可能被系统标记为"正在使用"。追加完就
close()。
自测三问(点开对答案)
1. **"追加"有哪两条实现路径?各有什么特点?** ——**① 打开时用 `ios::app`**:写位置**每次都强制到末尾**(**`seekp` 也拉不回来**),最省心,适合日志;**② 打开后 `seekp(0, ios::end)`**:手动搬一次写位置,**之后还能自由挪**,适合"追加完还要回头改"。 2. **用 `fstream` 以 `ios::in | ios::out` 打开一个已有文件,直接 `<<` 会发生什么?** ——**从文件开头开始覆盖**。**因为 `in | out` 打开时写位置在开头**(且不清空)。**要追加必须先 `seekp(0, ios::end)`。** 3. **为什么日志文件必须用 `ios::app` 打开?** ——**因为日志文件通常已经存在**,而**`ofstream` 的默认模式含 `trunc`,会把历史日志清空**。**`app` 既不删旧内容,又保证每次都写在末尾**,正好符合日志"只往后加"的性质。- ☐ 下一节(15.4.3 文件结尾的判断)