4. 标准I/O库:FILE流、fopen/fclose、fread/fwrite、fseek、缓冲机制
好,咱们今天聊聊标准I/O库。说实话,这是系统编程里最常用、也最容易出坑的一块。很多新手觉得文件操作嘛,不就是打开、读写、关闭?嗯,表面上看确实是这样。但等你真正遇到线上程序莫名其妙丢数据、性能上不去的时候,才会意识到——哦,原来缓冲机制才是灵魂。
4.1 FILE流:一个“包装盒”
先说说FILE流。你想想看,系统调用(比如read、write)是直接跟内核打交道的,每次调用都要陷入内核态,开销不小。标准I/O库在中间加了一层——FILE结构体。它把文件描述符fd、缓冲区、当前读写位置等信息打包在一起。
我个人习惯把FILE流理解成一个“包装盒”。你往盒子里放数据,它攒够了再一次性交给内核。这样效率高很多。我在项目中遇到过一个大文件处理的场景,用裸的系统调用逐字节读,慢得让人抓狂。换成fread后,速度直接起飞。
核心要点:FILE流是带缓冲的,系统调用是不带缓冲的。这是两者最本质的区别。
4.2 fopen / fclose:开门与关门
fopen打开一个文件,返回一个FILE*指针。fclose关闭它,释放资源。这俩成对出现,缺一不可。
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("fopen failed");
return -1;
}
// ... 读写操作 ...
fclose(fp);
模式参数有很多:
| 模式 | 含义 | 文件不存在时 |
|---|---|---|
| "r" | 只读 | 返回NULL |
| "w" | 只写,清空原内容 | 创建新文件 |
| "a" | 追加写 | 创建新文件 |
| "r+" | 读写,不会清空 | 返回NULL |
| "w+" | 读写,清空原内容 | 创建新文件 |
我曾经犯过一个低级错误:用"r"模式打开一个不存在的文件,没检查返回值就直接往下读写。结果程序崩溃了。记住:每次fopen后一定要检查fp是否为NULL。
4.3 fread / fwrite:批量搬运工
fread和fwrite是标准I/O库的核心读写函数。它们操作的是缓冲区,不是直接跟磁盘打交道。
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
参数有点绕:size是每个元素的大小,nmemb是元素个数。返回值是实际读写的元素个数。举个例子:
char buf[1024];
size_t ret = fread(buf, 1, sizeof(buf), fp);
if (ret != sizeof(buf)) {
if (feof(fp)) {
printf("读到文件末尾了\n");
} else if (ferror(fp)) {
printf("读取出错\n");
}
}
这里有个坑:fread返回值小于请求数,不一定是出错。可能是读到文件末尾了。所以要用feof和ferror来区分。
我在项目中遇到过一个问题:用fwrite写数据,程序正常退出,但文件内容不完整。后来发现是缓冲区没刷出去。嗯,这就引出了缓冲机制。
4.4 缓冲机制:三种模式
标准I/O库有三种缓冲模式:
- 全缓冲:缓冲区满了才刷。普通磁盘文件默认是全缓冲。
- 行缓冲:遇到换行符就刷。终端设备(stdout)默认是行缓冲。
- 无缓冲:每次写入立即刷。stderr默认是无缓冲。
为什么会这样设计?你想想看,磁盘文件追求吞吐量,攒一批再写效率高。终端追求实时性,打一行字就显示出来。错误信息需要立即输出,所以无缓冲。
可以用setvbuf来修改缓冲模式:
setvbuf(fp, NULL, _IONBF, 0); // 无缓冲
setvbuf(fp, NULL, _IOLBF, 0); // 行缓冲
setvbuf(fp, buf, _IOFBF, sizeof(buf)); // 全缓冲,使用自定义缓冲区
避坑指南:如果你在写日志文件,程序崩溃了,最后几条日志可能还在缓冲区里没写出去。我建议关键日志用fflush强制刷,或者用无缓冲模式。
4.5 fseek:文件定位
fseek可以移动文件读写位置。原型:
int fseek(FILE *stream, long offset, int whence);
whence有三种:
- SEEK_SET:从文件开头算起
- SEEK_CUR:从当前位置算起
- SEEK_END:从文件末尾算起
举个例子:
fseek(fp, 0, SEEK_END); // 跳到文件末尾
long size = ftell(fp); // 获取文件大小
fseek(fp, 0, SEEK_SET); // 跳回开头
这里要注意:fseek会清除缓冲区中的未写入数据。如果你先fwrite了一些数据,还没刷出去,然后fseek到别的位置,那些数据就丢了。
我曾经在写一个数据库索引文件时,先写了几条记录,然后fseek回去修改头部信息。结果发现之前写的数据没了。排查了半天,才发现是fseek把缓冲区清掉了。解决方案是先fflush,再fseek。
4.6 知识体系总览
下面这张图展示了标准I/O库的核心逻辑:
说白了,标准I/O库就是在应用程序和系统调用之间加了一层缓冲。这一层让我们的代码更简洁、更高效,但也带来了缓冲相关的坑。理解缓冲机制,是掌握标准I/O库的关键。
总结一下:
- FILE流是带缓冲的,系统调用是不带缓冲的
- fopen/fclose要成对使用,记得检查返回值
- fread/fwrite的返回值要结合feof/ferror来判断
- 缓冲模式有三种:全缓冲、行缓冲、无缓冲
- fseek会清除缓冲区,先fflush再fseek
好了,标准I/O库就聊到这儿。记住一句话:缓冲区是你的朋友,也是你的敌人。用好了效率翻倍,用不好数据丢失。多写代码,多踩坑,自然就熟练了。