5. 字符读写函数:fgetc与fputc函数详解、EOF的判定、字符读写示例
字符读写,是文件操作里最基础、也最容易被忽视的一环。很多人一上来就搞fread、fwrite大块读写,结果遇到文本文件里的换行符、EOF判定,反而栽了跟头。我个人习惯,但凡处理文本文件,优先用fgetc和fputc——简单、可控、不容易出幺蛾子。
5.1 fputc:写一个字符到文件
函数原型长这样:
int fputc(int ch, FILE *stream);
参数ch是你想写入的字符(注意是int类型,不是char),stream是文件指针。返回值:写入成功返回刚写入的字符,失败返回EOF。
我刚开始学的时候有个疑问:为什么字符参数是int?后来踩坑才明白——因为EOF是-1,而char可能是unsigned char(0~255),没法表示-1。所以标准库干脆用int来传递字符,这样EOF就能被正确识别。
看个简单例子:
#include <stdio.h>
int main() {
FILE *fp = fopen("test.txt", "w");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
char str[] = "Hello, 嵌入式!";
int i = 0;
while (str[i] != '\0') {
fputc(str[i], fp);
i++;
}
fclose(fp);
return 0;
}
这段代码把字符串逐个字符写入文件。注意我用了while循环,而不是for——其实都行,个人习惯而已。
5.2 fgetc:读一个字符从文件
函数原型:
int fgetc(FILE *stream);
每次调用,从文件当前位置读取一个字符,然后文件位置指针自动后移。返回值:读取成功返回字符的ASCII码(int类型),读到文件末尾或出错返回EOF。
看个读取的例子:
#include <stdio.h>
int main() {
FILE *fp = fopen("test.txt", "r");
if (fp == NULL) {
perror("打开文件失败");
return 1;
}
int ch;
while ((ch = fgetc(fp)) != EOF) {
putchar(ch); // 输出到屏幕
}
fclose(fp);
return 0;
}
这里有个经典陷阱:ch必须声明为int,不能是char。为什么?因为EOF是-1,如果ch是char(假设是unsigned char),那-1会被截断成255,永远不等于EOF,循环就死循环了。我曾经在项目里犯过这个错,调试了一下午才发现。
5.3 EOF的判定:到底怎么才算结束?
EOF是End Of File的缩写,定义在stdio.h里,值通常是-1。但EOF的判定,其实有两种情况:
- 真的读到文件末尾——此时feof(fp)返回非零值。
- 读取出错——此时ferror(fp)返回非零值。
这两种情况,fgetc都会返回EOF。所以光靠判断返回值是不是EOF,你分不清是正常结束还是出错了。严谨的做法是:
int ch;
while ((ch = fgetc(fp)) != EOF) {
// 处理字符
}
if (feof(fp)) {
printf("正常读到文件末尾\n");
} else if (ferror(fp)) {
printf("读取过程中发生错误\n");
}
嗯,这里要注意:feof()不是提前判断文件是否结束,而是在读取操作之后判断是否因为到达末尾而结束。很多人误以为feof()能预判未来,其实它只能回顾历史。
- fgetc返回EOF ≠ 文件结束,也可能是错误
- 用feof()和ferror()区分两种情况
- feof()必须在读取操作之后调用,不能提前
5.4 字符读写示例:文件复制
把上面的知识串起来,写一个文件复制程序。说白了就是从一个文件读字符,写到另一个文件:
#include <stdio.h>
#include <stdlib.h>
int main() {
FILE *src, *dst;
int ch;
src = fopen("source.txt", "rb");
if (src == NULL) {
perror("无法打开源文件");
return 1;
}
dst = fopen("dest.txt", "wb");
if (dst == NULL) {
perror("无法打开目标文件");
fclose(src);
return 1;
}
while ((ch = fgetc(src)) != EOF) {
fputc(ch, dst);
}
if (feof(src)) {
printf("文件复制完成\n");
} else if (ferror(src)) {
printf("读取源文件时出错\n");
}
fclose(src);
fclose(dst);
return 0;
}
这个例子我用了二进制模式"rb"和"wb"。为什么?因为如果是文本模式,Windows下换行符\r\n会被自动转换,导致复制出来的文件字节数不一样。你想想看,如果复制一个二进制文件(比如图片),被改了换行符,那图片就打不开了。
5.5 字符读写的性能问题
有人会问:fgetc/fputc一次只读写一个字节,效率是不是太低了?
其实标准库内部有缓冲区。fgetc不是每次真的去读磁盘,而是先从磁盘读一大块到缓冲区,然后从缓冲区里取一个字符给你。所以性能没那么差。我在项目里用fgetc处理过几百MB的日志文件,速度完全可以接受。
但如果你追求极致性能,或者处理的是大文件(GB级别),那还是建议用fread/fwrite。字符读写更适合小文件、文本处理、或者需要逐字符分析的场景。
5.6 知识体系图
下面这张图,把字符读写的核心逻辑串起来了:
5.7 避坑指南
最后,把我这些年踩过的坑总结一下:
- 不要用char接收fgetc返回值——EOF会变成255,死循环等着你。
- feof()不能提前判断——它只告诉你"上次读取是否因为到达末尾而结束",不能预知未来。
- 文本模式 vs 二进制模式——Windows下文本模式会自动转换换行符,处理非文本文件一定要用二进制模式。
- fputc写入后记得fflush或fclose——数据还在缓冲区里,没真正写到磁盘。程序崩溃就全丢了。
- 检查fopen返回值——NULL指针直接拿来用,段错误没跑。
我曾经在一个嵌入式项目里,用fgetc逐字符解析传感器数据流。因为偷懒用了char接收返回值,结果在特定数据组合下程序卡死。查了两天才发现是EOF判定出了问题。从那以后,我写文件读取代码,第一件事就是检查变量类型是不是int。
字符读写虽然基础,但基础不牢,地动山摇。把这些细节吃透,后面学fread/fwrite、格式化读写,才能事半功倍。
公众号:蓝海资料掘金营,微信deep3321