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

一个文件流内部藏着两个"位置标记": - 读位置(get pointer) ——下一次从哪儿读(管它的是 seekg / tellg); - 写位置(put pointer) ——下一次往哪儿写(管它的是 seekp / tellp)。 "移动"就是改它们:seek 是"挪",tell 是"问现在在哪儿"。 一句话概括:顺序读写是"一路向前",加了位置操作才叫"随机存取" ——这是从"能读文件"到"能当简易数据库用"的分水岭。

📝 本节和前面几节的关系

前面所有读写都是从头到尾顺序走的,本节给这个流程装上"方向盘": - 15.3.4 的二进制 + 定长记录,配上本节的位置操作,就成了 15.4.4 的"随机访问"; - 15.3.5 求文件大小用的 seekg(0, ios::end) + tellg() 就是本节的内容; - 15.1.3 的 ios::beg / cur / end 在这里正式上岗(那时只是列了表)。 一句话:本节把"文件"从"一卷纸"变成"一张能跳着翻的纸"。

一、两个位置,为什么是两个

流内部其实是"两个指针":

        ┌──────────────────────────────────────────┐
        │  文件内容:A B C D E F G H ...            │
        └──────────────────────────────────────────┘
                 ↑                    ↑
           读位置(get)           写位置(put)
         "下次从这儿读"         "下次往这儿写"

为什么读和写要分开? 想想 fstream 同时读写一个文件的场景:

fstream f("a.txt", ios::in | ios::out);
// 你希望:① 从第 0 字节读一段;② 把结果写到第 100 字节;③ 再回来从第 10 字节继续读

如果共用一个位置,这三个动作会互相踩脚。 分开之后互不干扰。

对 ifstream(只读)来说:只有读位置有意义(写位置用不了)。 对 ofstream(只写)来说:只有写位置有意义。

四个成员函数,各管一个:

函数 管谁 名字怎么来的
seekg(pos) 读位置(get) "g" = get = 读
tellg() 问读位置 同上
seekp(pos) 写位置(put) "p" = put = 写
tellp() 问写位置 同上

记住"g 管读、p 管写"就不会混 ——和 getline、put 的字母是同一套逻辑。

二、seek 的两种用法

形式一:绝对定位(跳到第 N 个字节)

in.seekg(0);          // 跳到开头(最常用的一句)
in.seekg(10);         // 跳到第 10 个字节

形式二:相对定位(从某个基准挪一段)

in.seekg(偏移量, 基准);

三个基准(15.1.3 的第 2 组常量):

基准 意思 例子
ios::beg 从文件开头算 in.seekg(5, ios::beg); → 跳到第 5 字节
ios::cur 从当前位置算 in.seekg(3, ios::cur); → 往后挪 3 字节
ios::end 从文件末尾算 in.seekg(-5, ios::end); → 末尾往前 5 字节

注意:

三、两个最常用的套路

套路一:求文件大小(三行)

in.seekg(0, ios::end);        // ① 挪到末尾
long long size = in.tellg();  // ② 问"现在在哪儿" —— 那就是总字节数
in.seekg(0, ios::beg);        // ③ 挪回开头(不然后面读不到东西)

为什么第 ③ 步不能省? 因为seekg 是"改位置",不是"读一下就走" ——挪到末尾之后,不回开头,后面的读会立刻失败(因为已到末尾)。

这个"忘记归位"是本节最常见的初级错误。

套路二:读完之后重新读一遍

// 第一次读完
while (getline(in, line)) { ... }

// 想再读一遍
in.clear();                   // ★ 先清错误状态(读到头时置了 eofbit/failbit)
in.seekg(0, ios::beg);        // 再回到开头
while (getline(in, line)) { ... }

为什么必须先 clear()?

因为流一旦进入失败状态(到末尾读失败,就是失败状态),后面的 seekg 也会被忽略 ——"一个坏了的流,连挪位置都不肯挪"。 先治好它(clear),它才肯听你的话。(15.4.1 会讲清这一点。)

四、动手 Demo:位置操作的三件事

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

int main() {
    const string fname = "seek_demo.txt";

    // 准备:写一串有规律的字符
    {
        ofstream o(fname);
        if (!o) { cerr << "写不了" << endl; return 1; }
        o << "ABCDEFGHIJ";                 // 10 个字符,没有换行
        o.close();
    }

    ifstream in(fname);
    if (!in) { cerr << "读不了" << endl; return 1; }

    // ── ① 求文件大小 ──────────────────────
    in.seekg(0, ios::end);
    long long size = (long long)in.tellg();
    cout << "① 文件大小 = " << size << " 字节" << endl;

    // ── ② 回到开头,读 3 个字符 ────────────
    in.seekg(0, ios::beg);
    cout << "② 回到开头,tellg=" << (long long)in.tellg() << ",读 3 个:";
    for (int i = 0; i < 3; i++) cout << (char)in.get();
    cout << "  现在 tellg=" << (long long)in.tellg() << endl;

    // ── ③ 从第 7 个字节开始读 ──────────────
    in.seekg(7, ios::beg);
    cout << "③ 跳到第 7 字节,读 3 个:";
    for (int i = 0; i < 3; i++) cout << (char)in.get();
    cout << endl;

    // ── ④ 相对挪动:从当前位置往后 1 个 ─────
    in.seekg(1, ios::cur);                 // 现在在第 10 字节(末尾)
    cout << "④ 当前位置(末尾往后挪 1)can read? "
         << (in.get() ? "能" : "不能(到末尾了)") << endl;
    in.close();

    // ── ⑤ 不 clear() 就重读,会怎样 ────────
    ifstream in2(fname);
    string s;
    while (getline(in2, s)) { }            // 读空(这里只有一个"行")
    cout << "⑤ 读完一遍后 seekg(0) 再 get:" << endl;
    in2.seekg(0, ios::beg);                // ★ 故意不 clear()
    cout << "   不 clear 直接读 -> " << (in2.get() == EOF ? "失败" : "成功") << endl;
    in2.clear();                           // ★ 先清状态
    in2.seekg(0, ios::beg);                // 再归位
    cout << "   先 clear 再读   -> " << (in2.get() == EOF ? "失败" : "成功") << endl;
    in2.close();
    return 0;
}

看到什么算成功:

① 文件大小 = 10 字节
② 回到开头,tellg=0,读 3 个:ABC  现在 tellg=3
③ 跳到第 7 字节,读 3 个:HIJ
④ 当前位置(末尾往后挪 1)can read? 不能(到末尾了)
⑤ 读完一遍后 seekg(0) 再 get:
   不 clear 直接读 -> 失败
   先 clear 再读   -> 成功

读结果:

五、30 秒实验

  1. 把 ② 里的 in.seekg(0, ios::beg); 注释掉,直接读 → 读不出来(位置还在末尾)。证明"求大小之后必须归位"。
  2. 把 ① 和 ② 之间的 seekg(0, ios::beg) 改成 seekg(-10, ios::end) → 也回到开头(-10 从末尾往前 10 = 位置 0)。两种写法等价,体会"基准"的作用。
  3. 把 ⑤ 的 in2.clear() 删掉再跑 → 从"成功"变成"失败"。这一对输出就是本节最值钱的两行。

六、常见误区

  1. "seekg 是读一下文件" ——它是"挪位置",不读任何数据。挪完还得自己读。
  2. "求完文件大小可以直接接着读" ——不行,位置在末尾,必须先 seekg(0) 归位。
  3. "seekg(0) 就能重新读一遍" ——前提是流的状态是好的。如果上一轮读到末尾失败了,必须先 clear()(本节 Demo ⑤)。
  4. "tellg() 返回的就是文件大小" ——只在"位置在末尾"时成立。它返回的是"当前位置",不一定等于文件大小。
  5. "seekg 和 seekp 可以随便混用" ——seekg 动读位置、seekp 动写位置,在 ifstream 上 seekp 无效(只读流没有写位置),在 ofstream 上 seekg 无效。
  6. "文本文件也能随便 seekg 到任意位置" ——不推荐。文本模式可能存在换行符转换,你算的"第 N 个字节"和磁盘上的字节对不上。要随机访问就用二进制模式(15.4.4)。
自测三问(点开对答案) 1. **流里为什么有"两个位置"?分别由哪几个函数管?** ——**因为 `fstream` 要同时读写,共用一个位置会互相踩脚。** **读位置由 `seekg` / `tellg` 管(g = get),写位置由 `seekp` / `tellp` 管(p = put)。** 2. **"求文件大小"的三行标准写法是什么?最后一行为什么不能省?** ——**`seekg(0, ios::end);` → `tellg()` → `seekg(0, ios::beg);`。** **最后一行不能省,因为前两步把读位置留在了末尾**,**不归位后面就读不到东西。** 3. **读了一遍文件之后想再读一遍,为什么要先 `clear()`?** ——**因为"读到末尾失败"会把流置为失败状态**,**而失败状态的流会忽略后续的 `seekg`**。**必须先 `clear()` 恢复状态,再 `seekg(0)` 归位。**

相关笔记