第1章:文件操作(一)——文件指针、打开关闭与基本读写

各位同学,咱们今天正式进入文件操作的世界。说实话,文件操作是C语言里非常实用的一块内容。我做了这么多年嵌入式开发,几乎每个项目都离不开它——从配置文件读取、日志记录,到数据采集存储,全得靠文件操作来搞定。

你想想看,程序跑起来之后,数据存在内存里,一断电就全没了。那怎么行?得存到文件里,下次启动还能读回来。这就是文件操作的价值所在。

1.1 文件指针(FILE*)——文件操作的“遥控器”

在C语言里,所有文件操作都离不开一个核心角色——FILE结构体指针。说白了,FILE*就是你和文件之间的“遥控器”。你通过它来指挥文件读写,而不是直接去操作硬盘上的数据块。

核心概念:FILE是一个结构体类型,定义在中。它包含了文件的各种状态信息,比如当前读写位置、缓冲区状态、错误标志等。我们不需要关心它的内部细节,只需要知道怎么用它的指针就行。

// 声明一个文件指针
FILE *fp;

嗯,这里要注意:FILE是个结构体,但我们在代码里从来不会去定义FILE类型的变量,只会定义指向它的指针。为什么?因为文件的状态信息是由系统管理的,我们只需要一个“遥控器”去操作它。

我个人习惯给文件指针变量起名叫fp或者pFile,这样一看就知道是文件指针。当然,你叫ffile也行,但团队协作时最好统一命名风格。

1.2 文件的打开与关闭——fopen / fclose

操作文件的第一步,就是打开它。就像你要用一本书,得先把它从书架上拿下来一样。

1.2.1 fopen——打开文件

FILE *fopen(const char *filename, const char *mode);

这个函数接收两个参数:文件名和打开模式。返回值是一个FILE*指针,如果打开失败,返回NULL

常见的打开模式:

模式 含义 文件不存在时 文件存在时
"r" 只读 返回NULL 正常打开
"w" 只写 创建新文件 清空原内容
"a" 追加 创建新文件 保留原内容,在末尾写入
"r+" 读写 返回NULL 正常打开
"w+" 读写 创建新文件 清空原内容
"a+" 读写追加 创建新文件 保留原内容,在末尾写入

我曾经踩过的坑:用"w"模式打开文件时,如果文件已经存在,内容会被直接清空!有一次我在调试日志系统时,不小心用了"w"而不是"a",结果把之前所有的日志都覆盖了。从那以后,我每次写fopen都会先确认模式对不对。

1.2.2 fclose——关闭文件

int fclose(FILE *stream);

文件用完了,一定要关掉。这就像借了书看完要还回去一样。不关闭文件会导致资源泄漏,严重时可能让程序崩溃。

为什么一定要关?因为文件缓冲区里的数据可能还没真正写到磁盘上。调用fclose时,系统会先把缓冲区里的数据刷出去,再释放资源。

FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
    printf("文件打开失败!\n");
    return -1;
}

// ... 进行文件操作 ...

fclose(fp);  // 别忘了关!

小技巧:我习惯在打开文件后立即检查返回值,并在函数末尾统一关闭。如果函数中有多个退出点,可以用goto语句集中处理资源释放。虽然goto名声不好,但在资源管理上它确实好用。

1.3 文件的读写——字符与字符串操作

文件打开之后,就可以开始读写了。咱们先从最基础的字符和字符串操作说起。

1.3.1 fgetc——读一个字符

int fgetc(FILE *stream);

这个函数从文件中读取一个字符。注意返回值是int而不是char,为什么?因为除了返回字符的ASCII码,它还要返回一个特殊值——EOF(End Of File),表示文件读完了。EOF通常是-1,而char类型可能无法表示负数。

FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
    printf("打开失败\n");
    return -1;
}

int ch;
while ((ch = fgetc(fp)) != EOF) {
    putchar(ch);  // 输出到屏幕
}

fclose(fp);

你看这个循环,每次读一个字符,直到遇到EOF。这是最经典的文件读取方式之一。

1.3.2 fputc——写一个字符

int fputc(int c, FILE *stream);

把一个字符写入文件。成功时返回写入的字符,失败返回EOF

FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
    printf("打开失败\n");
    return -1;
}

fputc('H', fp);
fputc('e', fp);
fputc('l', fp);
fputc('l', fp);
fputc('o', fp);

fclose(fp);

这段代码会在output.txt里写入"Hello"。当然,实际项目中很少这样一个个字符写,但理解原理很重要。

1.3.3 fgets——读一行字符串

char *fgets(char *s, int size, FILE *stream);

从文件中读取一行,最多读取size-1个字符,遇到换行符或EOF停止。读取的字符串末尾会自动加上'\0'

重要:fgets会把换行符'\n'也读进来,这一点和gets不同。很多新手在这里栽跟头,读出来的字符串末尾多了个换行符,还得手动去掉。

FILE *fp = fopen("config.txt", "r");
if (fp == NULL) {
    printf("打开失败\n");
    return -1;
}

char buffer[256];
while (fgets(buffer, sizeof(buffer), fp) != NULL) {
    // 去掉末尾的换行符
    size_t len = strlen(buffer);
    if (len > 0 && buffer[len-1] == '\n') {
        buffer[len-1] = '\0';
    }
    printf("读取到: %s\n", buffer);
}

fclose(fp);

我在项目中经常用fgets来读取配置文件,比如WiFi名称、密码之类的。一行一个配置项,非常方便。

1.3.4 fputs——写一行字符串

int fputs(const char *s, FILE *stream);

把一个字符串写入文件。注意它不会自动加换行符,需要你自己加。

FILE *fp = fopen("log.txt", "a");
if (fp == NULL) {
    printf("打开失败\n");
    return -1;
}

fputs("系统启动成功\n", fp);
fputs("温度传感器初始化完成\n", fp);

fclose(fp);

你看,用"a"模式打开,每次写入都追加到文件末尾,非常适合做日志记录。

1.4 知识体系总览

说了这么多,咱们用一张图来梳理一下本章的知识结构:

文件操作(一)知识体系 文件操作 FILE* 文件指针 fopen / fclose 读写操作 结构体类型 状态信息封装 打开模式:r/w/a 关闭与资源释放 fgetc / fputc fgets / fputs 核心:打开 → 操作 → 关闭

1.5 避坑指南与实战建议

最后,我把自己这些年踩过的坑总结一下,希望对你有帮助:

  • 检查返回值:每次调用fopen后,一定要检查返回值是否为NULL。我曾经在一个嵌入式项目里忘了检查,结果文件系统挂载失败,程序直接崩溃了。
  • 关闭文件:打开的文件一定要关闭。不关的话,文件描述符会泄漏。Linux系统默认一个进程只能打开1024个文件,泄漏多了就再也打不开新文件了。
  • 缓冲区问题:写入文件后,数据可能还在缓冲区里。如果想立即写入磁盘,可以调用fflush(fp)强制刷新。这在写日志时特别有用,防止程序崩溃丢失数据。
  • 路径问题:在Windows和Linux下,文件路径的写法不同。Windows用反斜杠\,Linux用正斜杠/。建议统一用正斜杠,或者用宏定义来处理。
  • 文本模式 vs 二进制模式:Windows下文本模式会把\n自动转换成\r\n,而Linux不会。跨平台开发时要注意这一点,用"b"标志(如"rb"、"wb")可以强制使用二进制模式。

我的习惯:在嵌入式开发中,我经常用fgets配合sscanf来解析配置文件。比如读取一行"ssid=MyWiFi",然后用sscanf提取出键和值。这样代码清晰,也容易维护。

好了,这一章的内容就到这里。文件操作是C语言里非常实用的技能,掌握了这些基础,后面咱们再聊更高级的文件操作函数,比如freadfwritefseek这些。记住:打开、操作、关闭,三步走,别漏了任何一步。


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