第1章:文件操作(一)——文件指针、打开关闭与基本读写
各位同学,咱们今天正式进入文件操作的世界。说实话,文件操作是C语言里非常实用的一块内容。我做了这么多年嵌入式开发,几乎每个项目都离不开它——从配置文件读取、日志记录,到数据采集存储,全得靠文件操作来搞定。
你想想看,程序跑起来之后,数据存在内存里,一断电就全没了。那怎么行?得存到文件里,下次启动还能读回来。这就是文件操作的价值所在。
1.1 文件指针(FILE*)——文件操作的“遥控器”
在C语言里,所有文件操作都离不开一个核心角色——FILE结构体指针。说白了,FILE*就是你和文件之间的“遥控器”。你通过它来指挥文件读写,而不是直接去操作硬盘上的数据块。
核心概念:FILE是一个结构体类型,定义在
// 声明一个文件指针
FILE *fp;
嗯,这里要注意:FILE是个结构体,但我们在代码里从来不会去定义FILE类型的变量,只会定义指向它的指针。为什么?因为文件的状态信息是由系统管理的,我们只需要一个“遥控器”去操作它。
我个人习惯给文件指针变量起名叫fp或者pFile,这样一看就知道是文件指针。当然,你叫f、file也行,但团队协作时最好统一命名风格。
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 知识体系总览
说了这么多,咱们用一张图来梳理一下本章的知识结构:
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语言里非常实用的技能,掌握了这些基础,后面咱们再聊更高级的文件操作函数,比如fread、fwrite、fseek这些。记住:打开、操作、关闭,三步走,别漏了任何一步。