17、文件IO基础:文件指针(FILE*)、文本模式与二进制模式、缓冲机制

文件IO,说白了就是程序跟磁盘文件打交道的方式。我刚开始学C语言那会儿,总觉得文件操作不就是读读写写嘛,有啥难的?后来真到项目里才发现,这里面的坑一个比一个深。今天咱们就把文件IO的底裤扒干净,从FILE*指针到缓冲机制,一个一个说清楚。

17.1 文件指针(FILE*)—— 文件操作的身份证

在C语言里,所有文件操作都离不开一个东西——FILE*。你可以把它理解成文件的“身份证”。每次打开文件,系统都会给你分配一个FILE结构体,里面存着文件的各种状态信息:当前读写位置、缓冲区状态、错误标志等等。

核心要点:FILE* 是一个指向结构体的指针,不是文件本身。它更像是一个“遥控器”,你通过它来操控文件。

来看个最简单的例子:

#include <stdio.h>

int main() {
    FILE *fp = fopen("test.txt", "r");
    if (fp == NULL) {
        perror("文件打开失败");
        return -1;
    }
    
    // 文件操作...
    
    fclose(fp);  // 千万别忘了关
    return 0;
}

这里有个细节我特别想强调:fopen返回NULL的情况一定要处理。我在项目中遇到过好几次,同事写的代码直接拿fp就用,结果文件路径写错了,程序直接崩溃。你想想看,线上环境出这种问题多尴尬。

注意:fclose() 不仅要调用,还要确保在正确的时机调用。文件指针泄漏是C程序里最常见的资源泄漏问题之一。

17.2 文本模式 vs 二进制模式 —— 别搞混了

fopen的第二个参数决定了文件的打开模式。很多人觉得“r”和“rb”没啥区别,嗯,在Linux上确实差别不大,但在Windows上,区别可就大了去了。

模式 含义 换行处理 适用场景
"r" / "w" / "a" 文本模式 自动转换 \r\n ↔ \n 纯文本文件(.txt, .csv, .html)
"rb" / "wb" / "ab" 二进制模式 不做任何转换 图片、视频、可执行文件等

为什么会这样?因为Windows和Linux对换行的定义不同。Windows用\r\n(回车+换行),Linux只用\n(换行)。文本模式下,C标准库会自动帮你做转换。

我的建议:如果你不确定文件类型,一律用二进制模式。尤其是处理非文本文件时,用文本模式可能会破坏数据。我曾经见过有人用"w"模式写图片文件,结果每个0x0A前面都被插入了0x0D,图片直接打不开了。

看个对比:

// 文本模式写入
FILE *fp = fopen("test.txt", "w");
fputs("hello\nworld", fp);  // Windows上实际写入的是 "hello\r\nworld"
fclose(fp);

// 二进制模式写入
FILE *fp2 = fopen("test.bin", "wb");
fputs("hello\nworld", fp2); // 写入的就是 "hello\nworld",不做转换
fclose(fp2);

17.3 缓冲机制 —— 性能的关键

缓冲机制是文件IO里最容易忽略但又最重要的部分。说白了,就是系统为了减少磁盘读写次数,先把数据攒一攒,攒够了再一次性写入。

C语言支持三种缓冲模式:

  • 全缓冲:缓冲区满了才刷新。默认用于普通磁盘文件。
  • 行缓冲:遇到换行符就刷新。默认用于终端(stdout)。
  • 无缓冲:每次写入都直接操作。默认用于stderr。

你可以用setvbuf函数来修改缓冲模式:

#include <stdio.h>

int main() {
    FILE *fp = fopen("output.txt", "w");
    if (fp == NULL) return -1;
    
    char buffer[4096];
    // 设置为全缓冲,缓冲区大小4KB
    setvbuf(fp, buffer, _IOFBF, sizeof(buffer));
    
    // 或者设置为行缓冲
    // setvbuf(fp, NULL, _IOLBF, 0);
    
    // 或者设置为无缓冲
    // setvbuf(fp, NULL, _IONBF, 0);
    
    fputs("hello", fp);  // 不会立即写入磁盘
    fputs("world", fp);  // 还是不会写入
    
    fflush(fp);  // 手动刷新缓冲区
    fclose(fp);
    return 0;
}

关键点:程序正常退出时,所有缓冲区会自动刷新。但如果程序崩溃了,缓冲区里的数据就丢了。这就是为什么日志系统通常要设置无缓冲或者行缓冲。

我给你们画个图,把整个文件IO的流程理清楚:

C语言文件IO缓冲机制流程图 用户程序 标准IO库(fread/fwrite/fputs等) 全缓冲 (_IOFBF) 缓冲区满才刷新 默认用于磁盘文件 行缓冲 (_IOLBF) 遇到换行符刷新 默认用于stdout 无缓冲 (_IONBF) 立即写入/读取 默认用于stderr 内核缓冲区(Page Cache) 磁盘文件

从图上你能看到,数据从用户程序到磁盘,中间经过了至少两层缓冲。标准IO库的缓冲区是第一层,内核的Page Cache是第二层。这就是为什么有时候你fwrite了数据,程序断电重启后数据却没了——数据还在内核缓冲区里没刷到磁盘上。

实用技巧:

  • 写日志文件时,建议用行缓冲或手动fflush,防止程序崩溃丢日志
  • 处理大文件时,全缓冲性能最好,但要注意内存占用
  • stderr默认无缓冲,所以错误信息能立即看到——这是设计者故意的

17.4 避坑指南

最后分享几个我踩过的坑:

  • 文件指针泄漏:每次fopen都要配一个fclose。我见过最离谱的代码,一个循环里开了1000个文件没关,直接把系统的文件描述符耗尽了。
  • 缓冲区未刷新:程序异常退出时,fwrite的数据可能还在缓冲区里。关键数据写入后记得fflush。
  • 文本/二进制模式混用:在Windows上,用"r"模式读二进制文件,0x1A会被当成EOF提前结束读取。这个问题我排查了整整一个下午。
  • 忽略返回值:fopen、fwrite、fclose的返回值一定要检查。别觉得“不可能失败”,磁盘满了、权限不够、路径不存在,这些情况随时可能发生。

嗯,文件IO的基础就这些。记住一句话:文件操作无小事。每一个细节都可能在生产环境里给你上一课。


公众号:蓝海资料掘金营,微信deep3321