第二十八天:文件读写操作——fgetc, fputc, fgets, fputs, fscanf, fprintf, fread, fwrite
文件操作,说白了就是让程序跟硬盘上的数据打交道。我刚开始学C语言那会儿,总觉得文件操作可有可无,直到第一次做嵌入式项目,需要把传感器数据存到SD卡里……嗯,从那以后我再也不敢小看这八个函数了。
今天咱们就把这八个函数掰开揉碎了讲清楚。它们分成三组:字符级、字符串级、格式化级、二进制级。每一组都有各自的适用场景。
1. 字符级读写:fgetc 和 fputc
这两个函数是最基础的。一次只读写一个字符。你想想看,如果文件很大,一个一个字符读确实慢,但有些场景非它们不可。
核心区别:
- fputc(ch, fp):把一个字符 ch 写入文件指针 fp 指向的文件
- fgetc(fp):从文件指针 fp 指向的文件读一个字符,返回 int 类型
为什么 fgetc 返回 int 而不是 char?这个问题我当年也困惑过。原因很简单:文件结束符 EOF 是 -1,而 char 类型可能是无符号的,没法表示 -1。所以用 int 来接收,才能正确判断是否读到文件尾。
// 用 fgetc 和 fputc 实现文件拷贝
#include <stdio.h>
int main() {
FILE *src = fopen("source.txt", "r");
FILE *dst = fopen("dest.txt", "w");
if (src == NULL || dst == NULL) {
printf("文件打开失败\n");
return 1;
}
int ch; // 注意是 int,不是 char
while ((ch = fgetc(src)) != EOF) {
fputc(ch, dst);
}
fclose(src);
fclose(dst);
return 0;
}
我的经验:在嵌入式项目中,我经常用 fgetc 逐字符解析配置文件。因为配置文件通常不大,而且需要逐字符判断注释符号 '#' 或 ';'。用 fgets 读整行反而不好处理。
2. 字符串级读写:fgets 和 fputs
fgets 和 fputs 是处理文本行的利器。我个人习惯用 fgets 来读配置文件,因为它能自动处理缓冲区大小,防止溢出。
| 函数 | 原型 | 说明 |
|---|---|---|
| fgets | char *fgets(char *s, int size, FILE *stream) | 最多读 size-1 个字符,遇到换行或 EOF 停止,自动加 '\0' |
| fputs | int fputs(const char *s, FILE *stream) | 写入字符串 s,不自动加换行符 |
注意:fgets 会保留换行符 '\n',而 fputs 不会自动加换行符。我曾经在项目中因为忘记处理 fgets 读到的换行符,导致字符串比较一直失败,排查了半天才发现问题。
// 用 fgets 和 fputs 逐行读写文件
#include <stdio.h>
#include <string.h>
int main() {
FILE *fp = fopen("log.txt", "r");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
// 去掉行尾的换行符
size_t len = strlen(line);
if (len > 0 && line[len-1] == '\n') {
line[len-1] = '\0';
}
printf("读取到: %s\n", line);
}
fclose(fp);
return 0;
}
3. 格式化读写:fscanf 和 fprintf
这两个函数跟 scanf/printf 用法几乎一样,只是多了一个文件指针参数。说白了,就是把键盘输入换成文件输入,把屏幕输出换成文件输出。
我在项目中用 fprintf 写日志文件特别多。比如记录传感器数据时,用格式化输出可以很方便地控制数据对齐和精度。
// 用 fprintf 写结构化数据
#include <stdio.h>
typedef struct {
int id;
char name[32];
float score;
} Student;
int main() {
FILE *fp = fopen("students.txt", "w");
if (fp == NULL) return 1;
Student stu = {1001, "张三", 95.5};
fprintf(fp, "%d,%s,%.1f\n", stu.id, stu.name, stu.score);
fclose(fp);
// 用 fscanf 读回来
fp = fopen("students.txt", "r");
if (fp == NULL) return 1;
Student read_stu;
fscanf(fp, "%d,%[^,],%f", &read_stu.id, read_stu.name, &read_stu.score);
printf("读回: %d %s %.1f\n", read_stu.id, read_stu.name, read_stu.score);
fclose(fp);
return 0;
}
避坑指南:fscanf 遇到空格或换行就会停止读取。如果字段里包含空格,用 %[^,] 这种格式来指定分隔符。我曾经用 fscanf 读 CSV 文件时,因为没处理好逗号分隔,数据全乱了。
4. 二进制读写:fread 和 fwrite
fread 和 fwrite 是真正的大杀器。它们直接读写内存块,不关心数据格式。在嵌入式开发中,我经常用它们来读写结构体、数组、甚至整个配置块。
| 函数 | 原型 | 说明 |
|---|---|---|
| fwrite | size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) | 从 ptr 写入 nmemb 个大小为 size 的数据块 |
| fread | size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) | 从文件读 nmemb 个大小为 size 的数据块到 ptr |
你想想看,用 fprintf 写一个结构体,每个字段都要格式化,读回来还要解析。用 fwrite 直接写内存映像,一个函数调用就搞定了。效率高得多。
// 用 fwrite 和 fread 读写结构体数组
#include <stdio.h>
#define MAX_STUDENTS 100
typedef struct {
int id;
char name[32];
float scores[3];
} Student;
int main() {
Student students[MAX_STUDENTS];
// 假设已经填充了数据...
FILE *fp = fopen("students.dat", "wb");
if (fp == NULL) return 1;
// 一次性写入整个数组
size_t written = fwrite(students, sizeof(Student), MAX_STUDENTS, fp);
printf("成功写入 %zu 个学生\n", written);
fclose(fp);
// 读回来
Student read_back[MAX_STUDENTS];
fp = fopen("students.dat", "rb");
if (fp == NULL) return 1;
size_t read = fread(read_back, sizeof(Student), MAX_STUDENTS, fp);
printf("成功读取 %zu 个学生\n", read);
fclose(fp);
return 0;
}
重要提醒:二进制文件在不同平台间移植时要注意字节序(大小端)和结构体对齐问题。我在做跨平台项目时,就遇到过 ARM 和 x86 之间结构体对齐不一致导致数据错乱的情况。解决办法是手动序列化,或者用 __attribute__((packed)) 强制紧凑对齐。
5. 如何选择?一张表说清楚
| 场景 | 推荐函数 | 原因 |
|---|---|---|
| 逐字符解析(如解析注释) | fgetc / fputc | 灵活控制,逐字符判断 |
| 逐行读取配置文件 | fgets | 安全,自动处理缓冲区 |
| 写日志、格式化输出 | fprintf | 方便控制格式和对齐 |
| 读写结构化数据(结构体、数组) | fread / fwrite | 效率高,直接操作内存 |
| 解析 CSV 等分隔符文件 | fscanf | 格式化解析,配合 %[^,] 使用 |
6. 文件打开模式的小结
不管用哪个读写函数,第一步都是 fopen。打开模式选错了,后面全白搭。我整理了一份速查表:
| 模式 | 含义 | 文件不存在 | 文件存在 |
|---|---|---|---|
| "r" | 只读文本 | 返回 NULL | 正常打开 |
| "w" | 只写文本 | 创建新文件 | 清空内容 |
| "a" | 追加文本 | 创建新文件 | 追加到末尾 |
| "rb" | 只读二进制 | 返回 NULL | 正常打开 |
| "wb" | 只写二进制 | 创建新文件 | 清空内容 |
| "ab" | 追加二进制 | 创建新文件 | 追加到末尾 |
我的习惯:在嵌入式开发中,我几乎只用二进制模式 "rb" 和 "wb"。因为文本模式在 Windows 下会把 '\n' 转成 '\r\n',导致读写不一致。用二进制模式可以避免这种平台差异。
7. 别忘了 fclose
最后说一句:打开的文件一定要关闭。fclose 不仅释放资源,还会刷新缓冲区。我见过太多人写代码只 fopen 不 fclose,结果程序跑着跑着文件句柄耗尽,系统崩溃。
嗯,文件读写就讲到这里。这八个函数你掌握了,日常开发中 90% 的文件操作场景都能应付。剩下的 10%,无非是加一些错误处理和性能优化,那些咱们后面再聊。
公众号:蓝海资料掘金营,微信deep3321