10. 随机访问:fgetpos/fsetpos函数、大文件处理、fpos_t类型
文件操作里,最让我觉得「有灵魂」的功能,就是随机访问。
什么意思呢?就是你可以不按顺序读文件。想跳到第100个字节,直接跳过去;想回到开头,一个转身就回去。不用像磁带一样从头播到尾。
嗯,今天我们就聊聊这个。顺便把大文件处理也一并拿下。
10.1 为什么需要随机访问?
我刚开始写嵌入式程序时,处理的是日志文件。日志每天增长,几MB到几十MB不等。需求是:只读最后100行。
如果每次都从头读到尾,效率太低了。尤其在一些资源受限的MCU上,内存就那么几十KB,根本扛不住。
这时候,随机访问就派上用场了。直接跳到文件末尾附近,往回读一点,搞定。
核心思想:随机访问让你像操作数组一样操作文件。指定位置,直接读写。
10.2 fgetpos 和 fsetpos:一对好搭档
C标准库提供了两对函数来实现随机访问:
- 老牌组合:fseek / ftell —— 用long类型保存位置
- 现代组合:fgetpos / fsetpos —— 用fpos_t类型保存位置
我个人更推荐后者。为什么?因为fpos_t是一个抽象类型,不一定是整数。在某些系统上,它可能是一个结构体,能表示更大的文件偏移量。
10.2.1 fgetpos:获取当前位置
#include <stdio.h>
int fgetpos(FILE *stream, fpos_t *pos);
调用成功后,pos里存的就是当前文件位置。返回0表示成功,非0表示出错。
10.2.2 fsetpos:设置当前位置
#include <stdio.h>
int fsetpos(FILE *stream, const fpos_t *pos);
把文件位置设置到之前保存的pos处。同样,成功返回0,失败返回非0。
小提示:fsetpos只能跳转到之前用fgetpos保存过的位置。不能像fseek那样直接指定偏移量。这是它的限制,也是它的设计哲学——更安全,更可移植。
10.3 实战:用fgetpos/fsetpos实现文件断点续读
我记得有一次做数据采集项目,需要从一个大文件中分批读取记录。每次读100条,处理完再读下100条。如果程序中途崩溃,下次启动要从上次的位置继续。
解决方案很简单:每读完一批,用fgetpos保存位置到文件。下次启动时,用fsetpos跳回去。
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *fp = fopen("data.bin", "rb");
if (!fp) {
perror("打开文件失败");
return 1;
}
fpos_t pos;
char buffer[256];
// 假设之前保存的位置在 checkpoint.dat 里
FILE *ckpt = fopen("checkpoint.dat", "rb");
if (ckpt) {
fread(&pos, sizeof(pos), 1, ckpt);
fsetpos(fp, &pos);
fclose(ckpt);
printf("从断点恢复,继续读取...\n");
} else {
printf("没有断点,从头开始...\n");
}
// 读取并处理数据
while (fgets(buffer, sizeof(buffer), fp)) {
// 处理每一行...
printf("%s", buffer);
// 每处理100行,保存一次位置
static int count = 0;
if (++count % 100 == 0) {
fgetpos(fp, &pos);
ckpt = fopen("checkpoint.dat", "wb");
if (ckpt) {
fwrite(&pos, sizeof(pos), 1, ckpt);
fclose(ckpt);
}
printf("--- 已保存断点 ---\n");
}
}
fclose(fp);
return 0;
}
注意:fpos_t的大小和结构因平台而异。不要假设它就是一个long。保存到文件时,最好用sizeof(fpos_t)来读写,保证跨平台兼容性。
10.4 fpos_t类型:没那么简单
fpos_t到底是什么?
在大多数32位系统上,它就是一个long long或者__int64。但在某些嵌入式系统上,它可能是一个结构体,包含文件描述符和内部状态。
我曾经在某个RTOS上遇到过坑:那个平台的fpos_t是一个结构体,包含两个成员。我用fwrite(&pos, 8, 1, fp)只写了8个字节,结果结构体实际大小是12字节。下次读回来就全乱了。
所以,记住一条铁律:永远用sizeof(fpos_t)来获取大小,不要硬编码。
| 平台 | fpos_t 实际类型 | 大小(典型值) |
|---|---|---|
| Linux x86_64 | long long | 8字节 |
| Windows x64 | __int64 | 8字节 |
| 某些嵌入式RTOS | 结构体 | 8~16字节 |
| 旧式16位系统 | long | 4字节 |
10.5 大文件处理:突破2GB限制
说到大文件,就不得不提一个经典问题:2GB限制。
传统的fseek/ftell使用long类型保存偏移量。在32位系统上,long是4字节,最大只能表示2^31-1 = 2GB。超过2GB的文件,就无能为力了。
怎么解决?
10.5.1 方案一:使用64位系统
在64位系统上,long通常是8字节,可以表示最大9EB的文件。但前提是你的代码要编译成64位模式。
10.5.2 方案二:使用fgetpos/fsetpos
fpos_t是抽象类型,在支持大文件的系统上,它内部就是64位整数。用这对函数,天然支持大文件。
10.5.3 方案三:使用专有API
Linux上有fseeko/ftello,使用off_t类型。Windows上有_fseeki64/_ftelli64,直接使用__int64。
// Linux 上的大文件支持
#define _FILE_OFFSET_BITS 64
#include <stdio.h>
// 现在 fseeko/ftello 使用64位偏移
FILE *fp = fopen("huge_file.bin", "rb");
fseeko(fp, 5000000000LL, SEEK_SET); // 跳到5GB处
long long pos = ftello(fp);
printf("当前位置: %lld\n", pos);
我的建议:如果你要处理超过2GB的文件,直接用fgetpos/fsetpos。它们是最可移植的方案。除非你确定程序只跑在某个特定平台上,再用专有API。
10.6 知识体系:随机访问的核心逻辑
下面这张图,帮你理清今天讲的所有内容:
10.7 避坑指南:我踩过的几个坑
做嵌入式这么多年,在文件随机访问上栽过几次跟头。分享出来,大家别重蹈覆辙。
- 坑一:fpos_t不是long
我曾经在ARM Cortex-M3上,把fpos_t当成long来用,直接赋值。结果编译过了,运行就崩。后来一查,那个平台的fpos_t是个结构体。从此以后,我再也不敢对fpos_t做任何假设。 - 坑二:fsetpos后忘记清除缓冲区
有一次做数据解析,用fsetpos跳回文件开头重新读。结果读出来的数据还是旧的。为什么?因为stdio的缓冲区没清。解决方案:在fsetpos之后调用setbuf(fp, NULL)或者fflush(fp)。 - 坑三:大文件上的ftell返回负数
在32位系统上,如果文件超过2GB,ftell返回的long会溢出,变成负数。我当时调试了半天,还以为是文件损坏了。后来换成fgetpos才解决。
我的习惯:只要文件可能超过100MB,我就直接用fgetpos/fsetpos。省心,不用考虑平台差异。反正性能上差别不大,安全第一。
10.8 总结
随机访问是文件操作的高级技巧。掌握了它,你就能像操作内存一样操作文件。
fgetpos/fsetpos是现代C标准推荐的方案。它们用fpos_t抽象类型,屏蔽了底层差异。处理大文件时,这是最稳妥的选择。
记住:能用fgetpos,就别用ftell。能用fpos_t,就别用long。
嗯,今天就聊到这里。下次遇到需要跳着读文件的需求,你知道该用什么了。
公众号:蓝海资料掘金营,微信deep3321