3、文件的顺序读写(一):fputc、fgetc、fputs、fgets 函数的使用

文件操作里最基础、最常用的,就是顺序读写。说白了,就是从头到尾,一个字符一个字符,或者一行一行地读和写。今天咱们先聊四个最基础的函数:fputcfgetcfputsfgets

我个人习惯把这四个函数分成两组:字符级字符串级。字符级就是一次处理一个字符,字符串级就是一次处理一整行。咱们一个一个来看。

3.1 字符级读写:fputc 和 fgetc

3.1.1 fputc:写一个字符到文件

fputc 的用法很简单:把一个字符写到指定的文件流里。原型长这样:

int fputc(int ch, FILE *stream);

第一个参数是你要写的字符(注意是 int 类型,但实际传字符常量就行),第二个参数是文件指针。返回值:写成功返回你写的那个字符,失败返回 EOF

我在项目中遇到过这样一个场景:需要把一个字符串逐字符加密后写入文件。那时候就是用 fputc 一个字符一个字符地写,每写一个字符就做一次异或运算。嗯,虽然效率不高,但控制力极强。

核心要点:fputc 每次只写一个字符。如果你需要写大量数据,建议用缓冲区批量写入,否则性能会受影响。

来看个例子:

#include <stdio.h>

int main() {
    FILE *fp = fopen("test.txt", "w");
    if (fp == NULL) {
        perror("文件打开失败");
        return 1;
    }

    char str[] = "Hello, C!";
    int i = 0;
    while (str[i] != '\0') {
        fputc(str[i], fp);
        i++;
    }

    fclose(fp);
    return 0;
}

这段代码把字符串 "Hello, C!" 逐字符写入了 test.txt。你想想看,如果字符串很长,这样写确实有点慢。但好处是——你可以在写入每个字符前做点手脚,比如加密、过滤、转换格式。

3.1.2 fgetc:从文件读一个字符

有写就有读。fgetc 从文件流中读取下一个字符。原型:

int fgetc(FILE *stream);

返回值:读取成功返回字符的 ASCII 码值(转为 int),读到文件末尾或出错返回 EOF

注意:fgetc 返回的是 int 类型,不是 char。为什么?因为 EOF 是 -1,而 char 可能是无符号的 0~255,无法表示 -1。所以一定要用 int 接收返回值,否则可能永远检测不到文件结尾。

我曾经踩过这个坑——用 char ch 接收 fgetc 的返回值,结果文件明明读完了,循环就是不停。后来查了半天才发现是类型问题。嗯,这个坑我帮你踩过了,你记住就好。

看个例子:

#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;
}

这段代码把 test.txt 的内容逐字符读到屏幕上。注意那个 while 条件——先赋值,再比较,这是 C 语言里非常经典的写法。

3.2 字符串级读写:fputs 和 fgets

字符级读写虽然灵活,但效率低。如果你要处理的是整行文本,用 fputsfgets 更合适。

3.2.1 fputs:写一个字符串到文件

fputs 把一个字符串写入文件,不包含字符串结尾的 \0。原型:

int fputs(const char *str, FILE *stream);

返回值:成功返回非负数(通常是 0),失败返回 EOF

注意:fputs 不会自动加换行符。如果你需要换行,得自己在字符串里加 \n。这和 puts 不同——puts 会自动加换行,但 fputs 不会。

小技巧:如果你想把多行文本写入文件,可以用 fputs 配合循环,每行末尾加 \n。我个人习惯在字符串末尾直接包含换行符,这样代码更清晰。

看例子:

#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "w");
    if (fp == NULL) {
        perror("文件打开失败");
        return 1;
    }

    fputs("第一行内容\n", fp);
    fputs("第二行内容\n", fp);
    fputs("第三行内容\n", fp);

    fclose(fp);
    return 0;
}

这段代码写入了三行文本。注意每个字符串末尾都有 \n,这样文件里就是三行。

3.2.2 fgets:从文件读一个字符串

fgets 从文件读取一行,最多读取 n-1 个字符,遇到换行符或文件末尾停止。原型:

char *fgets(char *str, int n, FILE *stream);

参数说明:

  • str:存放读取内容的缓冲区
  • n:缓冲区大小(最多读取 n-1 个字符,留一个给 \0
  • stream:文件指针

返回值:成功返回 str,读到文件末尾或出错返回 NULL

重要特性:fgets 会读取换行符并存入缓冲区。也就是说,如果一行有 10 个字符加一个换行符,fgets 会把这 11 个字符都读进来,然后在末尾加 \0。这一点和 gets 完全不同——gets 会丢弃换行符,但 gets 不安全,已经被废弃了。

看个例子:

#include <stdio.h>

int main() {
    FILE *fp = fopen("data.txt", "r");
    if (fp == NULL) {
        perror("文件打开失败");
        return 1;
    }

    char buffer[256];
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("读取到: %s", buffer);
    }

    fclose(fp);
    return 0;
}

这段代码逐行读取 data.txt 的内容并打印到屏幕。注意 printf 里没有加 \n,因为 fgets 已经把换行符读进来了。

3.3 四个函数的对比总结

为了方便你对比,我整理了一张表:

函数 操作单位 是否包含换行符 返回值 典型用途
fputc 1 个字符 不自动加 写入的字符 或 EOF 逐字符写入、加密、过滤
fgetc 1 个字符 会读取 读取的字符 或 EOF 逐字符读取、解析
fputs 1 个字符串 不自动加 非负数 或 EOF 写入整行文本
fgets 1 行(最多 n-1 字符) 会读取并保留 缓冲区指针 或 NULL 逐行读取文本

3.4 知识体系图

下面这张图帮你理清这四个函数的关系和适用场景:

文件顺序读写核心函数 文件流 FILE* 字符级 字符串级 fputc fgetc fputs fgets 适用场景对比: • 字符级(fputc/fgetc):适合逐字符处理,如加密、格式转换、流式解析 • 字符串级(fputs/fgets):适合逐行处理,如配置文件读取、日志写入 • 选择原则:需要精细控制选字符级,追求效率选字符串级

3.5 避坑指南

最后,分享几个我实际开发中遇到的坑:

  • fgetc 返回值类型:一定要用 int 接收,别用 char。否则 EOF 检测会失效。
  • fgets 缓冲区大小:第二个参数一定要传真实大小,别传小了导致截断,也别传大了浪费内存。我一般用 sizeof(buffer)
  • fputs 不加换行:如果你用 fputs 写文件,记得在字符串里加 \n,否则所有内容会挤在一行。
  • 文件打开模式:写文件用 "w" 会清空原有内容,追加用 "a"。我曾经因为忘了这个,把配置文件清空了……嗯,从那以后我每次打开文件前都会确认模式。

这四个函数虽然基础,但用好了能解决 80% 的文本文件读写需求。下一节咱们继续聊 fprintffscanf,它们更强大,也更灵活。


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