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 知识体系:随机访问的核心逻辑

下面这张图,帮你理清今天讲的所有内容:

随机访问与大文件处理知识体系 文件随机访问 传统方法:fseek/ftell 现代方法:fgetpos/fsetpos 使用 long 类型,最大2GB 32位系统上无法处理大文件 使用 fpos_t 抽象类型 天然支持大文件(64位) 可移植性更好 推荐:优先使用 fgetpos/fsetpos

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