7、格式化读写:fscanf函数、fprintf函数、格式化字符串的注意事项、sscanf/sprintf的变体

格式化读写,说白了就是让数据按照你指定的格式进出文件。我刚开始学C语言那会儿,觉得printf和scanf就够用了,直到第一次做嵌入式日志系统,才发现文件格式化读写才是真正的硬核工具。今天咱们就把这块彻底讲透。

7.1 fprintf:把数据“打印”到文件里

fprintfprintf 长得像亲兄弟,区别就在于多了一个文件指针参数。它的原型是:

int fprintf(FILE *stream, const char *format, ...);

第一个参数是文件指针,后面就跟 printf 一模一样了。返回值是实际写入的字符数,如果出错返回负数。

来看个实际例子:

#include <stdio.h>

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

    int id = 101;
    float temp = 25.6f;
    int humidity = 68;

    int written = fprintf(fp, "传感器ID:%d, 温度:%.1f°C, 湿度:%d%%\n",
                         id, temp, humidity);
    printf("成功写入 %d 个字符\n", written);

    fclose(fp);
    return 0;
}

输出到文件的内容是:传感器ID:101, 温度:25.6°C, 湿度:68%。注意那个 %%,在格式化字符串里要输出百分号必须写两个。

我的习惯:每次调用 fprintf 后,我都会检查返回值。曾经在一个数据采集项目中,SD卡写满了,fprintf 返回了负数,如果没检查,数据丢了都不知道。

7.2 fscanf:从文件里“扫描”数据

fscanfscanf 的文件版本,从文件流中读取格式化数据:

int fscanf(FILE *stream, const char *format, ...);

返回值是成功匹配并赋值的参数个数。如果遇到文件末尾,返回 EOF

读取刚才写入的日志文件:

#include <stdio.h>

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

    int id, humidity;
    float temp;

    int result = fscanf(fp, "传感器ID:%d, 温度:%f°C, 湿度:%d%%",
                        &id, &temp, &humidity);

    if (result == 3) {
        printf("读取成功: ID=%d, 温度=%.1f, 湿度=%d\n",
               id, temp, humidity);
    } else {
        printf("格式不匹配,只匹配了 %d 个字段\n", result);
    }

    fclose(fp);
    return 0;
}
我曾经踩过的坑:fscanf 的格式字符串必须和文件内容严格匹配,包括空格、逗号、换行符。有一次我多写了一个空格,结果匹配失败,查了半天才发现。建议用 fgets 先读一行,再用 sscanf 解析,这样更灵活。

7.3 格式化字符串的注意事项

格式化字符串看着简单,但坑不少。我总结了几条铁律:

  • 类型必须匹配:%d 对应 int,%f 对应 float,%lf 对应 double。搞错了不会报错,但数据会乱掉。
  • 缓冲区溢出:用 %s 时一定要指定宽度,比如 %19s,否则输入超过缓冲区长度就崩了。
  • %% 输出百分号:格式化字符串里想输出 %,必须写两个。
  • 空格和换行敏感:fscanf 默认会跳过空白字符(空格、制表符、换行),但如果你在格式串里写了空格,它也会匹配空白。
格式符 对应类型 常见陷阱
%d int 不要用于 long 或 short
%f float 读取 double 要用 %lf
%s char[] 必须指定宽度防溢出
%x unsigned int 读取十六进制时注意大小写

7.4 sscanf 和 sprintf:内存中的格式化读写

这两个函数不操作文件,而是操作内存中的字符串。说白了,就是把字符串当成文件来读写。

int sprintf(char *buffer, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

看个实际场景:解析 GPS 数据

#include <stdio.h>

int main() {
    char gps_data[] = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47";
    char time[10], lat[12], lon[12];
    int fix_quality, satellites;

    // 用 sscanf 解析
    int fields = sscanf(gps_data,
                        "$GPGGA,%[^,],%[^,],N,%[^,],E,%d,%d",
                        time, lat, lon, &fix_quality, &satellites);

    if (fields == 5) {
        printf("时间: %s\n", time);
        printf("纬度: %s N\n", lat);
        printf("经度: %s E\n", lon);
        printf("定位质量: %d\n", fix_quality);
        printf("卫星数: %d\n", satellites);
    }

    // 用 sprintf 组装新数据
    char output[128];
    sprintf(output, "GPS|%s|%s|%s|%d|%d",
            time, lat, lon, fix_quality, satellites);
    printf("组装结果: %s\n", output);

    return 0;
}
核心区别:fprintf/fscanf 操作文件,sprintf/sscanf 操作内存字符串。在嵌入式系统中,我经常先用 sprintf 组装好一行数据,再一次性用 fwrite 写入文件,这样效率更高。

7.5 知识体系总览

下面这张图帮你理清格式化读写的全家桶关系:

格式化读写函数族 输出类 fprintf(文件指针, 格式, ...) sprintf(缓冲区, 格式, ...) 输入类 fscanf(文件指针, 格式, &变量) sscanf(字符串, 格式, &变量) 操作文件用 f 版本,操作内存用 s 版本

7.6 避坑指南与实战建议

格式化读写看着简单,但实际项目中容易出问题。我分享几个经验:

  • 不要用 fscanf 解析复杂文本:如果文件格式复杂,先用 fgets 读一行,再用 sscanf 解析。这样出错时容易定位。
  • sprintf 的缓冲区要够大:我习惯用 snprintf 替代 sprintf,可以指定最大长度,防止溢出。
  • 检查返回值:fprintf 返回写入的字符数,fscanf 返回匹配的字段数。这两个返回值一定要检查。
  • 二进制 vs 文本:格式化读写是文本模式,适合人类可读的配置文件、日志等。如果是传感器数据、图像等,用二进制读写更高效。
我的小技巧:在嵌入式项目中,我经常用 sprintf 组装一条完整的日志,然后一次性写入文件。这样比多次调用 fprintf 效率高,而且能保证一行日志的原子性。

格式化读写是C语言文件操作的核心技能。你想想看,无论是写配置文件、解析数据、还是记录日志,都离不开这几个函数。掌握好它们,你的文件操作能力会上一个大台阶。


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