指针与文件操作:FILE 指针、文件读写缓冲区、二进制文件与文本文件的指针操作
文件操作,说白了就是让程序跟磁盘上的数据打交道。很多初学者觉得文件操作就是 fopen、fread、fwrite 那点事,其实不然。指针在这里扮演的角色,远比你想的要深。
我个人习惯把文件操作看作「内存与外设之间的数据搬运」。而 FILE 指针,就是这场搬运的总调度。
FILE 指针:不是文件,是控制块
很多人以为 FILE 指针指向的是磁盘上的文件。错。它指向的是一个结构体——文件控制块。这个结构体里装着文件当前位置、缓冲区状态、错误标志等信息。
我在项目中遇到过一位同事,他试图把 FILE 指针直接写入磁盘,说「这样下次启动就能直接恢复文件状态」。结果当然是崩溃。FILE 指针是运行时结构,进程一结束就失效了。
核心理解:FILE *fp 不是文件本身,而是文件操作的「遥控器」。你通过它告诉系统:我要读、我要写、我要跳到第几个字节。
// 正确的打开方式
FILE *fp = fopen("data.bin", "rb");
if (fp == NULL) {
perror("文件打开失败");
return -1;
}
// 操作...
fclose(fp); // 别忘了关
警告:fclose 之后,fp 变成野指针。我建议立即置为 NULL,否则后面误用会出诡异 bug。
文件读写缓冲区:看不见的加速器
你有没有想过,为什么 fread 一次读 1 字节和读 1024 字节,速度差那么多?
原因就在缓冲区。标准 I/O 库默认会给每个打开的文件分配一个缓冲区(通常是 4KB 或 8KB)。你调用 fgetc 时,它其实是一次性从磁盘读一大块到缓冲区,然后每次从缓冲区取一个字节给你。
我曾经调试过一个嵌入式设备上的日志系统。每次写一行日志就调用 fflush,结果整个系统卡得像蜗牛。去掉 fflush 后,速度提升了 10 倍。嗯,缓冲区就是用来减少磁盘访问次数的,你非要每次都刷,那还不如直接用 unbuffered I/O。
技巧:用 setvbuf 可以自定义缓冲区。我在处理大文件流时,会把缓冲区设到 64KB,吞吐量明显提升。
// 自定义缓冲区示例
char mybuf[65536];
FILE *fp = fopen("largefile.dat", "rb");
setvbuf(fp, mybuf, _IOFBF, sizeof(mybuf));
// 现在 fread 会使用 64KB 的缓冲区
| 缓冲模式 | 宏定义 | 行为 |
|---|---|---|
| 全缓冲 | _IOFBF | 缓冲区满才写磁盘,性能最好 |
| 行缓冲 | _IOLBF | 遇到换行符就写,适合终端输出 |
| 无缓冲 | _IONBF | 立即写,stderr 默认就是这个 |
二进制文件 vs 文本文件:指针的视角差异
从指针的角度看,二进制文件和文本文件的区别,本质上是「数据解释方式」的区别。
文本文件里,你看到的是字符。'1' 和 1 是两码事。二进制文件里,数据就是内存的原始映像。你用 fread 读一个 int,直接得到的就是 int 的二进制表示。
我记得有一次做跨平台项目,在 Windows 上写的二进制文件拿到 Linux 上读,全乱了。原因很简单:Windows 的文本模式会把 '\n' 转成 '\r\n',而二进制模式不会。如果你用 "w" 模式写二进制数据,系统会偷偷给你加回车,数据就坏了。
铁律:操作二进制文件,永远用 "wb" 或 "rb"。操作文本文件,用 "w" 或 "r"。不要混用。
// 二进制写入结构体
typedef struct {
int id;
char name[32];
float score;
} Student;
Student stu = {1001, "张三", 95.5f};
FILE *fp = fopen("student.dat", "wb");
fwrite(&stu, sizeof(Student), 1, fp);
fclose(fp);
// 二进制读取
Student stu2;
fp = fopen("student.dat", "rb");
fread(&stu2, sizeof(Student), 1, fp);
fclose(fp);
文件位置指针:你的定位器
每个文件流内部都有一个位置指针,指向当前读写的位置。fread 和 fwrite 会自动移动它。但有时候你需要手动跳转——这就是 fseek 和 ftell 的用武之地。
我做过一个日志分析工具,日志文件很大,每次只读最后 1KB。用 fseek(fp, -1024, SEEK_END) 直接跳到末尾往前 1024 字节,比从头读到尾快了不知道多少倍。
// 跳到文件末尾前 100 字节
fseek(fp, -100, SEEK_END);
// 获取当前偏移
long pos = ftell(fp);
printf("当前位置:%ld\n", pos);
// 回到文件开头
rewind(fp); // 等价于 fseek(fp, 0, SEEK_SET)
注意:fseek 对文本文件的定位不一定准确,因为文本模式下换行符的转换会导致实际字节数和逻辑字节数不一致。二进制模式下没问题。
知识体系总览
下面这张图,是我梳理的指针与文件操作的核心脉络。你看一眼就能明白各个概念之间的关系。
避坑指南:我踩过的那些坑
- 忘记 fclose:我曾经写过一个服务程序,循环打开文件但不关闭。跑了三天后,系统报「打开文件过多」。嗯,文件描述符泄漏了。
- feof 的误用:很多人用 while(!feof(fp)) 来读文件。这是错的。feof 只有在读取操作失败后才会设置标志。正确做法是检查 fread 或 fgetc 的返回值。
- 二进制模式下的换行符:在 Windows 上,如果你用 "w" 写二进制数据,0x0A 会被自动转成 0x0D 0x0A。数据就坏了。我吃过这个亏,后来一律用 "wb"。
- 缓冲区未刷新:程序异常退出时,缓冲区里的数据可能没写进磁盘。我建议在关键位置手动 fflush,或者用 setbuf(fp, NULL) 关闭缓冲。
// 正确的文件读取循环
size_t n;
char buf[256];
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
// 处理读到的 n 个字节
}
if (ferror(fp)) {
perror("读取错误");
}
个人习惯:我写文件操作代码时,一定会检查每个函数的返回值。fopen 返回 NULL、fwrite 返回 0、fseek 返回非 0——这些都是信号。忽略它们,就是在给自己埋雷。
文件操作说到底,就是管理好三个东西:FILE 指针的生命周期、缓冲区的刷新时机、位置指针的移动策略。把这三点吃透了,你就能在嵌入式系统里游刃有余地处理各种文件读写场景。