fscanf 是 C 标准库中一个强大的函数,用于从文件流中按照指定的格式读取数据,它的功能与 scanf 类似,但 scanf 是从标准输入(通常是键盘)读取,而 fscanf 可以从任何打开的文件流读取。

(图片来源网络,侵删)
函数原型
#include <stdio.h> int fscanf(FILE *stream, const char *format, ...);
参数详解
-
*`FILE stream`**:
- 这是一个指向
FILE结构体的指针,代表一个已经打开的文件流。 - 你通常通过
fopen()函数获取这个指针。 - 如果传入
stdin,fscanf的行为就和scanf完全一样。
- 这是一个指向
-
*`const char format`**:
- 这是一个格式字符串,用于定义你期望从文件中读取的数据类型和格式。
- 它包含两种类型的字符:
- 普通字符:必须与文件中的内容完全匹配(包括空格)。
- 格式说明符:以 开头,用于指定如何读取和转换后续的参数。
%d表示读取一个整数,%f表示读取一个浮点数,%s表示读取一个字符串。
-
(可变参数):
- 这是用于存储读取数据的变量列表。
- 你必须提供与格式字符串中格式说明符数量和类型相匹配的变量地址(即使用
&运算符取地址)。
返回值
fscanf 的返回值非常重要,它表示成功匹配和赋值的字段数量。

(图片来源网络,侵删)
- 成功:
- 返回成功读取、赋值的字段数量。
- 如果读取到文件末尾,返回
EOF(End of File)。
- 失败:
- 如果发生读取错误(例如文件损坏),返回
EOF。 - 如果在第一个字段匹配失败就遇到了文件末尾,也返回
EOF。 - 如果在匹配过程中发生错误(期望一个数字但遇到了字母),则返回已经成功赋值的字段数量(可能为 0)。
- 如果发生读取错误(例如文件损坏),返回
常用格式说明符
| 说明符 | 类型 | 示例 |
|---|---|---|
%d |
十进制整数 | fscanf(fp, "%d", &num); |
%f |
浮点数 | fscanf(fp, "%f", &f_num); |
%lf |
双精度浮点数 | fscanf(fp, "%lf", &d_num); (注意:scanf 和 fscanf 中必须用 %lf 读取 double 类型) |
%c |
单个字符 | fscanf(fp, "%c", &ch); |
%s |
字符串(到空格) | fscanf(fp, "%s", str); (注意:str 必须是已分配足够空间的字符数组) |
%[...] |
匹配指定字符集 | fscanf(fp, "%[a-z]", str); // 读取所有小写字母 |
[^...] |
匹配非指定字符集 | fscanf(fp, "%[^\n]", line); // 读取直到换行符的整行 |
| 匹配一个 字符 | fscanf(fp, "%%"); |
完整示例
下面是一个完整的例子,演示如何创建一个数据文件,然后使用 fscanf 读取它。
步骤 1: 创建一个数据文件 data.txt
假设我们有以下内容的 data.txt 文件:
101 Alice 95.5
102 Bob 88.0
103 Carol 92.3
步骤 2: 编写 C 程序 read_data.c
#include <stdio.h>
#include <stdlib.h>
// 定义一个结构体来存储学生信息
typedef struct {
int id;
char name[50];
double score;
} Student;
int main() {
// 1. 打开文件
// "r" 表示以只读模式打开
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("Error opening file"); // perror 会打印出系统错误信息
return EXIT_FAILURE;
}
printf("--- Reading data from data.txt ---\n");
// 2. 读取数据
Student s;
// 循环读取,直到 fscanf 返回 EOF 或小于期望的字段数
// 我们期望每次成功读取 3 个字段: %d, %s, %lf
while (fscanf(fp, "%d %49s %lf", &s.id, s.name, &s.score) == 3) {
// fscanf 返回 3 表示成功读取了 id, name, score
printf("ID: %d, Name: %s, Score: %.2f\n", s.id, s.name, s.score);
}
// 3. 关闭文件
fclose(fp);
printf("--- Reading finished ---\n");
return EXIT_SUCCESS;
}
步骤 3: 编译和运行
- 将代码保存为
read_data.c。 - 确保在同一目录下有
data.txt文件。 - 打开终端,编译程序:
gcc read_data.c -o read_data
- 运行程序:
./read_data
预期输出
--- Reading data from data.txt ---
ID: 101, Name: Alice, Score: 95.50
ID: 102, Name: Bob, Score: 88.00
ID: 103, Name: Carol, Score: 92.30
--- Reading finished ---
fscanf 的注意事项和陷阱
-
%s的缓冲区溢出风险:fscanf(fp, "%s", str);会读取直到遇到空白字符(空格、制表符、换行符)为止。- 如果文件中的字符串超过了
str数组的大小,就会发生缓冲区溢出,导致未定义行为和安全漏洞。 - 解决方案: 像示例中那样,指定最大读取宽度。
%49s表示最多读取 49 个字符,并自动在末尾添加'\0',确保不会溢出 50 大小的数组。
-
文件末尾和错误的处理:
(图片来源网络,侵删)- 永远不要只依赖
fscanf的返回值是否为EOF,更健壮的方式是检查返回值是否小于你期望的字段数。 while (fscanf(...) == 3)是一个好习惯,如果某行数据不完整(比如只有 ID 和 Name,没有 Score),fscanf会返回 2,循环就会终止,而不会出错或读取下一行的数据。
- 永远不要只依赖
-
空白字符的处理:
- 在格式字符串中,空格、制表符、换行符会被
fscanf当作一个或多个空白字符来处理,用于跳过输入中的空白。 fscanf(fp, "%d %s", &num, str);和fscanf(fp, "%d \t \n %s", &num, str);的效果是一样的,它们都会读取一个整数,然后跳过任意数量的空白,再读取一个字符串。- 如果你需要精确匹配某个空白字符,你需要把它放在格式字符串里。
fscanf(fp, "%d,", &num);会读取一个整数,然后要求后面必须有一个逗号。
- 在格式字符串中,空格、制表符、换行符会被
-
%c的特殊行为:fscanf(fp, "%c", &ch);会读取文件中的任何单个字符,包括空白字符(空格、换行符等)。- 如果你只想读取非空白字符,可以在
%c前加一个空格:fscanf(fp, " %c", &ch);,这个空格会告诉fscanf先跳过所有空白字符,然后再读取一个字符。
替代方案
虽然 fscanf 很方便,但它也有一些缺点:
- 灵活性差:如果文件格式不固定(比如某些字段可能缺失),
fscanf会变得非常复杂和脆弱。 - 性能较低:相比
fgets+sscanf的组合,fscanf的性能通常较差。
对于更复杂的文件解析,推荐使用以下模式:
char line_buffer[256];
while (fgets(line_buffer, sizeof(line_buffer), fp) != NULL) {
// line_buffer 现在包含了文件的一行
// 使用 sscanf 从这一行字符串中解析数据
int id;
char name[50];
double score;
if (sscanf(line_buffer, "%d %49s %lf", &id, name, &score) == 3) {
// 成功解析
printf("Parsed from line: ID: %d, Name: %s, Score: %.2f\n", id, name, score);
} else {
// 解析失败,可以处理格式不正确的行
printf("Skipping malformed line: %s", line_buffer);
}
}
这种方法的优点是:
- 逐行处理:逻辑清晰,易于调试。
- 容错性强:即使某行数据格式错误,程序也能继续处理下一行,而不会直接退出或读取错误的数据。
| 特性 | fscanf |
fgets + sscanf |
|---|---|---|
| 适用场景 | 格式严格、简单的文件 | 格式可能不固定或复杂的文件 |
| 易用性 | 简单直接,一行代码搞定 | 需要两步,代码稍多 |
| 健壮性 | 较差,对格式变化敏感 | 较高,可以逐行处理和验证 |
| 性能 | 通常较低 | 通常较高 |
| 内存安全 | 容易出错(如 %s 溢出) |
更容易控制,fgets 可限制读取长度 |
对于初学者和简单的数据文件,fscanf 是一个很好的起点,但在实际项目中,特别是处理可能由用户生成或来源不明的文件时,更推荐使用 fgets + sscanf 的组合来获得更好的控制和健壮性。
