指针与文件操作:文件指针FILE*、文件读写中的指针操作、文件缓冲区与指针
文件操作,说白了就是让程序和磁盘上的数据打交道。很多初学者觉得文件操作就是fopen、fread、fwrite那几条API,背下来就行了。但我要说,如果你不理解背后的指针机制,迟早会踩坑。
我个人习惯把文件操作分成三个层次来理解:文件指针本身、读写过程中的位置指针、以及缓冲区里的隐式指针。咱们一个一个说。
1. 文件指针 FILE* 到底是什么?
很多人以为FILE*就是指向文件内容的指针。其实不是。FILE是一个结构体,里面装的是文件的各种状态信息。我当年刚入行时也犯过这个错,以为FILE*指向的是文件数据,结果用memcpy去拷贝,程序直接崩了。
FILE结构体里有什么?
- 文件描述符(整数,底层系统调用用)
- 文件读写位置指示器(当前位置偏移量)
- 缓冲区指针和缓冲区大小
- 错误标志和文件结束标志
- 操作模式(读、写、追加等)
// 典型的FILE结构体简化版
typedef struct {
int _fd; // 文件描述符
char* _buf; // 缓冲区指针
int _bufsize; // 缓冲区大小
int _pos; // 当前读写位置
int _flags; // 状态标志
// ... 还有其他成员
} FILE;
你看,FILE结构体里本身就藏着指针——缓冲区指针。这个指针指向一块内存区域,用来暂存读写数据。为什么要这么做?因为磁盘I/O太慢了,一次读一个字节和一次读4KB,时间差了好几个数量级。
2. 文件读写中的位置指针操作
每个打开的文件都有一个隐形的“位置指针”,它记录着你当前读到或写到了文件的哪个位置。这个指针不是FILE*,而是FILE结构体里的一个整数偏移量。
我记得有一次做日志解析程序,需要反复读取同一个文件的某一段。我一开始每次都fopen/fclose,性能差得要命。后来改用fseek来回跳转,速度直接提升了10倍。
关键API:
ftell(FILE* fp)— 获取当前位置(返回long类型偏移量)fseek(FILE* fp, long offset, int whence)— 设置位置rewind(FILE* fp)— 回到文件开头,相当于fseek(fp, 0, SEEK_SET)fgetpos/fsetpos— 处理大文件时用,支持64位偏移
#include <stdio.h>
int main() {
FILE* fp = fopen("data.bin", "rb");
if (!fp) return -1;
// 获取文件大小
fseek(fp, 0, SEEK_END);
long filesize = ftell(fp);
printf("文件大小: %ld 字节\n", filesize);
// 回到开头读取
rewind(fp);
// 跳到第100个字节
fseek(fp, 100, SEEK_SET);
// 从当前位置往后跳50字节
fseek(fp, 50, SEEK_CUR);
// 从末尾往前跳20字节
fseek(fp, -20, SEEK_END);
fclose(fp);
return 0;
}
这里有个坑:文本模式和二进制模式下,fseek的行为不一样。在Windows的文本模式下,换行符\r\n会被转换成\n,导致fseek计算的位置偏移不准确。我曾经因为这个bug排查了整整一个下午,最后发现是模式字符串写成了"r"而不是"rb"。
避坑指南:
- 二进制文件一定要用"b"模式打开,否则fseek可能不准
- ftell返回long,在32位系统上最大只能表示2GB文件
- fseek之后一定要检查返回值,失败时返回非0
- 不要对stdin、stdout、stderr使用fseek
3. 文件缓冲区与指针的微妙关系
文件缓冲区,说白了就是一块内存,用来暂存数据。标准I/O库默认会给每个打开的文件分配一个缓冲区。你调用fread/fwrite时,数据先到缓冲区,等缓冲区满了或者你主动fflush,才真正写入磁盘。
你想想看,这个缓冲区本质上就是一个字符数组,而操作这个数组自然离不开指针。FILE结构体里的_buf指针就指向这个缓冲区,_pos指针则记录当前在缓冲区中的位置。
// 手动设置缓冲区
#include <stdio.h>
int main() {
FILE* fp = fopen("output.txt", "w");
if (!fp) return -1;
// 分配一个自定义缓冲区
char mybuf[4096];
setbuf(fp, mybuf); // 设置全缓冲
// 或者用setvbuf更灵活
// setvbuf(fp, mybuf, _IOFBF, sizeof(mybuf));
fprintf(fp, "这些数据会先写入缓冲区\n");
fprintf(fp, "缓冲区满了或者fflush才会真正写入磁盘\n");
// 强制刷新
fflush(fp);
fclose(fp);
return 0;
}
三种缓冲模式:
| 模式 | 宏定义 | 行为 |
|---|---|---|
| 全缓冲 | _IOFBF | 缓冲区满了才刷新,普通文件默认 |
| 行缓冲 | _IOLBF | 遇到换行符就刷新,终端设备默认 |
| 无缓冲 | _IONBF | 每次读写都直接操作,stderr默认 |
嗯,这里要注意:如果你用setbuf传入自定义缓冲区,这个缓冲区的生命周期必须比FILE*长。我见过有人把局部数组传给setbuf,函数返回后缓冲区被销毁,FILE*还在用,结果就是野指针访问,程序随机崩溃。
4. 缓冲区与指针的实战案例
我曾经写过一个数据采集程序,需要从串口读取传感器数据并写入文件。一开始直接用fprintf逐行写入,结果发现数据有丢失。排查后发现是缓冲区没及时刷新,程序崩溃时缓冲区里的数据全丢了。
解决方案很简单:要么定期fflush,要么用setbuf(fp, NULL)设置无缓冲模式。但无缓冲模式性能太差,最后我选择了定时fflush + 信号处理的方式。
#include <stdio.h>
#include <signal.h>
FILE* g_fp = NULL;
void sig_handler(int signo) {
if (g_fp) {
fflush(g_fp); // 崩溃前强制刷新缓冲区
}
exit(1);
}
int main() {
signal(SIGSEGV, sig_handler);
signal(SIGINT, sig_handler);
g_fp = fopen("sensor.log", "w");
if (!g_fp) return -1;
// 设置行缓冲,每行都刷新
setvbuf(g_fp, NULL, _IOLBF, 0);
// 模拟数据采集
for (int i = 0; i < 1000; i++) {
fprintf(g_fp, "数据点 %d: 温度=%.2f\n", i, 25.0 + i * 0.1);
// 行缓冲模式下,遇到\n会自动刷新
}
fclose(g_fp);
return 0;
}
关于缓冲区指针的几个要点:
- FILE结构体内部的缓冲区指针是隐式的,你不需要直接操作它
- 但理解它的存在,能帮你解释很多奇怪的现象(比如为什么fread后数据没立即写入文件)
- 多线程环境下,缓冲区操作不是线程安全的,需要加锁
- fclose会自动刷新缓冲区并释放内部资源
5. 知识体系图
下面这张图把文件指针、位置指针和缓冲区指针的关系梳理清楚了。你看一眼就能明白这三者是怎么配合工作的。
从这张图可以看得很清楚:用户程序通过FILE*间接操作文件,FILE内部维护了位置指针和缓冲区指针。位置指针决定你读写的是文件的哪个位置,缓冲区指针决定数据暂存在哪里。系统调用层才是真正和硬件打交道的。
6. 总结几个实用经验
- 能用fread/fwrite就别用fgetc/fputc:批量读写效率高,减少系统调用次数
- 频繁读写小数据时考虑自己加一层缓冲区:标准库的缓冲区不一定适合你的场景
- 多进程写同一个文件要加锁:文件锁或者用O_APPEND模式打开
- 不要迷信fflush:它只保证数据到内核缓冲区,不保证写到磁盘(除非用O_SYNC)
- 文件操作一定要检查返回值:fopen返回NULL、fread返回0、fseek返回非0,都是出错的信号
指针与文件操作,说白了就是理解数据在内存和磁盘之间的流动。FILE*是入口,位置指针是坐标,缓冲区指针是缓存。搞懂了这三者的关系,文件操作这块你就拿捏住了。
公众号:蓝海资料掘金营,微信deep3321