一个文件流内部藏着两个"位置标记":
- 读位置(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 字节 |
注意:
- 用
ios::end时偏移量通常是负数("往末尾之前挪"); seekg(0, ios::beg)和seekg(0)等价(后者是前者的简写)。
三、两个最常用的套路
套路一:求文件大小(三行)
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 再读 -> 成功
读结果:
- ① 10 字节 =
"ABCDEFGHIJ"正好 10 个字符(用文本模式写的,没有换行符,所以没有平台差异)。 - ② 读完 3 个字符后
tellg变成 3 ——证明"读位置"是真的在动。 - ③
seekg(7)后读到的是H I J——第 0 个是A,所以第 7 个是H(A=0,B=1,C=2,D=3,E=4,F=5,G=6,H=7)。"下标从 0 开始"这个老朋友又出现了(7.1 讲过数组下标)。 - ④ 说明"挪到末尾之后就读不出东西了"。
- ⑤ 才是本节最重要的发现:读到头之后,流已经"坏了"(进入失败状态),光
seekg(0)治不好它,必须先clear()。
五、30 秒实验
- 把 ② 里的
in.seekg(0, ios::beg);注释掉,直接读 → 读不出来(位置还在末尾)。证明"求大小之后必须归位"。 - 把 ① 和 ② 之间的
seekg(0, ios::beg)改成seekg(-10, ios::end)→ 也回到开头(-10从末尾往前 10 = 位置 0)。两种写法等价,体会"基准"的作用。 - 把 ⑤ 的
in2.clear()删掉再跑 → 从"成功"变成"失败"。这一对输出就是本节最值钱的两行。
六、常见误区
- "
seekg是读一下文件" ——它是"挪位置",不读任何数据。挪完还得自己读。 - "求完文件大小可以直接接着读" ——不行,位置在末尾,必须先
seekg(0)归位。 - "
seekg(0)就能重新读一遍" ——前提是流的状态是好的。如果上一轮读到末尾失败了,必须先clear()(本节 Demo ⑤)。 - "
tellg()返回的就是文件大小" ——只在"位置在末尾"时成立。它返回的是"当前位置",不一定等于文件大小。 - "
seekg和seekp可以随便混用" ——seekg动读位置、seekp动写位置,在ifstream上seekp无效(只读流没有写位置),在ofstream上seekg无效。 - "文本文件也能随便
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)` 归位。**- ☐ 下一节(15.4.1 文件错误与状态)
相关笔记
- 实现文件复制 —— 上一节:
seekg/tellg第一次露脸 - 文件错误与状态 —— 下一节:
clear()到底清的是什么 - 在指定位置读-写文件 —— 位置操作 + 定长记录 = 随机访问
- ios类中的枚举常量 ——
ios::beg/cur/end - 一维数组的引用 —— "下标从 0 开始"
- 🔷 C++基础 · 索引 —— 全课路线图