28、文件操作(二):字符读写、字符串读写、格式化读写
文件操作这块,上一章我们聊了文件的打开与关闭。今天咱们深入一点,看看文件里具体怎么读写数据。
我个人习惯把文件读写分成三个层次:字符级、字符串级和格式化级。这三个层次,说白了就是「一个字一个字读」、「一行一行读」和「按格式解析读」。你想想看,实际开发中这三种场景都会遇到,一个都不能少。
一、字符读写:fgetc 与 fputc
这是最底层的读写方式。一次只操作一个字符。
fputc 的用法很简单:
int fputc(int ch, FILE *stream);
把字符 ch 写入文件流。成功返回写入的字符,失败返回 EOF。
fgetc 反过来:
int fgetc(FILE *stream);
从文件流读取一个字符。读到文件末尾或出错时返回 EOF。
核心要点:EOF 是 -1,而 char 类型可能是 unsigned。所以 fgetc 的返回值用 int 接收,别用 char。我见过不少新手在这上面翻车。
来个实际例子:复制一个文本文件。
#include <stdio.h>
int main() {
FILE *src = fopen("source.txt", "r");
FILE *dst = fopen("dest.txt", "w");
if (!src || !dst) {
printf("文件打开失败\n");
return 1;
}
int ch;
while ((ch = fgetc(src)) != EOF) {
fputc(ch, dst);
}
fclose(src);
fclose(dst);
return 0;
}
我的经验:字符读写虽然慢,但在处理二进制文件头、解析协议帧时特别好用。我曾经在嵌入式项目里用 fgetc 逐字节解析自定义通信协议,稳得很。
二、字符串读写:fgets 与 fputs
字符读写太慢了。实际开发中,我们更常用字符串读写。
fputs 把字符串写入文件:
int fputs(const char *str, FILE *stream);
成功返回非负数,失败返回 EOF。注意它不会自动加换行符。
fgets 从文件读取一行:
char *fgets(char *str, int n, FILE *stream);
读取最多 n-1 个字符,遇到换行符或 EOF 停止。读取的换行符会保留在字符串里,然后自动加 '\0'。
我曾经踩过的坑:fgets 的第二个参数 n 是缓冲区大小,不是「想读的字符数」。如果你写 fgets(buf, 5, fp),它最多读 4 个字符 + 1 个 '\0'。很多人写成 fgets(buf, strlen(buf), fp),结果 buf 根本没初始化,直接崩了。
看个例子:逐行读取配置文件。
#include <stdio.h>
int main() {
FILE *fp = fopen("config.ini", "r");
if (!fp) {
perror("打开文件失败");
return 1;
}
char line[256];
int line_num = 0;
while (fgets(line, sizeof(line), fp)) {
line_num++;
// 去掉末尾换行符
size_t len = strlen(line);
if (len > 0 && line[len-1] == '\n') {
line[len-1] = '\0';
}
printf("第%d行: %s\n", line_num, line);
}
fclose(fp);
return 0;
}
我个人的习惯:用 fgets 读取后,总是手动去掉末尾的换行符。因为很多字符串处理函数(比如 strcmp)不会忽略 '\n',留着它容易出 bug。
三、格式化读写:fscanf 与 fprintf
这是最高级的读写方式。直接按格式解析或输出数据。
fprintf 和 printf 几乎一样,只是多了一个文件指针参数:
int fprintf(FILE *stream, const char *format, ...);
fscanf 和 scanf 类似:
int fscanf(FILE *stream, const char *format, ...);
看个例子:把结构体数据写入文件,再读回来。
#include <stdio.h>
typedef struct {
int id;
char name[32];
float score;
} Student;
int main() {
Student stu = {1001, "张三", 89.5};
// 写入文件
FILE *fp = fopen("student.txt", "w");
if (!fp) return 1;
fprintf(fp, "%d %s %.1f\n", stu.id, stu.name, stu.score);
fclose(fp);
// 读取回来
Student stu2 = {0};
fp = fopen("student.txt", "r");
if (!fp) return 1;
fscanf(fp, "%d %s %f", &stu2.id, stu2.name, &stu2.score);
fclose(fp);
printf("读取结果: %d %s %.1f\n", stu2.id, stu2.name, stu2.score);
return 0;
}
注意:fscanf 遇到空格、换行符会自动分隔。如果字符串里包含空格,fscanf 会读不全。这时候用 fgets 更靠谱。
四、三种方式的对比
| 函数 | 读写单位 | 适用场景 | 注意事项 |
|---|---|---|---|
| fgetc / fputc | 1 个字符 | 逐字节处理、协议解析 | 返回值用 int 接收 |
| fgets / fputs | 1 行字符串 | 文本文件逐行处理 | 注意缓冲区大小和换行符 |
| fscanf / fprintf | 格式化数据 | 结构化数据读写 | 字符串含空格时慎用 fscanf |
五、避坑指南
我这些年做项目,文件操作踩过的坑不少。挑几个典型的说说:
- 忘记检查返回值:fopen 返回 NULL 不检查,后面直接读写,程序直接崩。每次打开文件后,养成检查的习惯。
- 缓冲区溢出:fgets 的第二个参数写错了,或者用 gets(这个函数已经被废弃了)。永远用 fgets,别用 gets。
- 文件未关闭:fclose 忘记调用,导致文件句柄泄漏。程序跑久了,系统资源耗尽。
- 文本模式 vs 二进制模式:Windows 下文本模式会把 '\n' 转成 '\r\n',二进制模式不会。跨平台时要注意。
我曾经犯过的错:在一个日志系统里,用 fscanf 读取包含时间戳的日志行。时间戳格式是 "2024-01-15 14:30:00",中间有空格。fscanf 读到空格就停了,后面的数据全乱了。后来改成 fgets + sscanf 才解决。
六、小结
文件读写就这三种方式,从字符到字符串再到格式化,一层比一层方便。实际开发中,我建议:
- 处理二进制数据或协议帧 → 用 fgetc/fputc
- 处理文本配置文件、日志文件 → 用 fgets/fputs
- 处理结构化的数值数据 → 用 fscanf/fprintf
嗯,文件操作这块,多写多练就熟了。记住一点:每次打开文件,都要记得关闭。这是最基本的素养。
公众号:蓝海资料掘金营,微信deep3321