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——其实都行,个人习惯而已。

小技巧: 写入中文时,fputc是按字节写入的。UTF-8编码下,一个汉字占3个字节。所以上面的"嵌入式"三个字,实际写入了9个字节。如果你用fputc写中文,记得文件打开模式要选二进制模式"wb",否则Windows下换行符会被自动转换,导致字节数对不上。

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,循环就死循环了。我曾经在项目里犯过这个错,调试了一下午才发现。

注意: fgetc的返回值一定要用int接收!用char接收EOF会出大问题。这是C语言初学者最容易踩的坑之一。

5.3 EOF的判定:到底怎么才算结束?

EOF是End Of File的缩写,定义在stdio.h里,值通常是-1。但EOF的判定,其实有两种情况:

  1. 真的读到文件末尾——此时feof(fp)返回非零值。
  2. 读取出错——此时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。字符读写更适合小文件、文本处理、或者需要逐字符分析的场景。

我的经验: 解析配置文件、处理CSV、逐行分析日志——这些场景用fgetc最顺手。处理音视频、固件镜像——老老实实用fread/fwrite。

5.6 知识体系图

下面这张图,把字符读写的核心逻辑串起来了:

字符读写核心逻辑 fputc 写入 fgetc 读取 EOF 判定 写入到文件流 从文件流读取 返回EOF (-1) feof() → 文件结束 ferror() → 读取错误 返回值判断 + feof/ferror 双重确认

5.7 避坑指南

最后,把我这些年踩过的坑总结一下:

  • 不要用char接收fgetc返回值——EOF会变成255,死循环等着你。
  • feof()不能提前判断——它只告诉你"上次读取是否因为到达末尾而结束",不能预知未来。
  • 文本模式 vs 二进制模式——Windows下文本模式会自动转换换行符,处理非文本文件一定要用二进制模式。
  • fputc写入后记得fflush或fclose——数据还在缓冲区里,没真正写到磁盘。程序崩溃就全丢了。
  • 检查fopen返回值——NULL指针直接拿来用,段错误没跑。

我曾经在一个嵌入式项目里,用fgetc逐字符解析传感器数据流。因为偷懒用了char接收返回值,结果在特定数据组合下程序卡死。查了两天才发现是EOF判定出了问题。从那以后,我写文件读取代码,第一件事就是检查变量类型是不是int。

字符读写虽然基础,但基础不牢,地动山摇。把这些细节吃透,后面学fread/fwrite、格式化读写,才能事半功倍。


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