7、格式化读写:fscanf函数、fprintf函数、格式化字符串的注意事项、sscanf/sprintf的变体
格式化读写,说白了就是让数据按照你指定的格式进出文件。我刚开始学C语言那会儿,觉得printf和scanf就够用了,直到第一次做嵌入式日志系统,才发现文件格式化读写才是真正的硬核工具。今天咱们就把这块彻底讲透。
7.1 fprintf:把数据“打印”到文件里
fprintf 和 printf 长得像亲兄弟,区别就在于多了一个文件指针参数。它的原型是:
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:从文件里“扫描”数据
fscanf 是 scanf 的文件版本,从文件流中读取格式化数据:
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 知识体系总览
下面这张图帮你理清格式化读写的全家桶关系:
7.6 避坑指南与实战建议
格式化读写看着简单,但实际项目中容易出问题。我分享几个经验:
- 不要用 fscanf 解析复杂文本:如果文件格式复杂,先用 fgets 读一行,再用 sscanf 解析。这样出错时容易定位。
- sprintf 的缓冲区要够大:我习惯用 snprintf 替代 sprintf,可以指定最大长度,防止溢出。
- 检查返回值:fprintf 返回写入的字符数,fscanf 返回匹配的字段数。这两个返回值一定要检查。
- 二进制 vs 文本:格式化读写是文本模式,适合人类可读的配置文件、日志等。如果是传感器数据、图像等,用二进制读写更高效。
我的小技巧:在嵌入式项目中,我经常用 sprintf 组装一条完整的日志,然后一次性写入文件。这样比多次调用 fprintf 效率高,而且能保证一行日志的原子性。
格式化读写是C语言文件操作的核心技能。你想想看,无论是写配置文件、解析数据、还是记录日志,都离不开这几个函数。掌握好它们,你的文件操作能力会上一个大台阶。
公众号:蓝海资料掘金营,微信deep3321