fscanf 是什么?
fscanf 是 C 标准库中的一个函数,它的名字来源于 "file scan formatted"(文件格式化扫描),它的作用类似于 scanf,但有一个关键区别:

scanf: 从 标准输入流(通常是键盘)读取格式化数据。fscanf: 从 指定的文件流(通常是文件)读取格式化数据。
fscanf 就是在文件上执行 scanf 操作。
函数原型
在 stdio.h 头文件中,fscanf 的原型如下:
int fscanf(FILE *stream, const char *format, ...);
参数详解
-
FILE *stream:- 这是一个指向
FILE结构体的指针,也就是我们常说的“文件指针”或“文件流”。 - 它代表了你要从哪个文件中读取数据,这个文件指针通常是由
fopen()函数打开文件后返回的。 FILE *fp = fopen("data.txt", "r");,fscanf的第一个参数就可以是fp。
- 这是一个指向
-
const char *format:
(图片来源网络,侵删)- 这是一个格式化字符串,它定义了你期望读取的数据的格式。
- 它的用法和
scanf中的格式化字符串完全一样。 - 可以包含:
- 格式说明符: 如
%d(整数),%f(浮点数),%s(字符串),%c(字符) 等。 - 普通字符: 空格、制表符、换行符等空白字符会匹配并跳过输入流中的连续空白字符,非空白字符则会精确匹配输入流中的对应字符。
- 修饰符: 如
%hd(短整型),%lf(双精度浮点数) 等。
- 格式说明符: 如
-
(可变参数):
- 这是 ,表示这是一个可变参数函数。
- 你需要提供一系列指针,用于存储从文件中读取到的数据。
- 非常重要: 这些参数必须是指针(变量的地址),因为
fscanf需要通过指针将读取到的值存回到你指定的内存位置。 - 如果你要读取一个整数,你需要提供一个整型变量的地址
&my_int。
返回值
fscanf 函数返回一个 int 类型的值,这个值有三种可能:
-
成功读取的项数:
- 如果函数成功执行,它会返回成功匹配并赋值的参数的个数。
fscanf(fp, "%d %f", &i, &f);如果成功读取了一个整数和一个浮点数,它会返回2。
-
EOF(End-File):
(图片来源网络,侵删)- 当函数读取到文件末尾,或者发生读取错误时,它会返回
EOF(通常定义为-1)。 - 这意味着文件中没有更多的数据可以读取了。
- 当函数读取到文件末尾,或者发生读取错误时,它会返回
-
部分匹配:
- 如果在读取过程中,输入流的数据与格式字符串不匹配,函数会立即停止,并已经成功匹配的项数。
- 格式字符串是
%d %s,但文件内容是"abc 123"。%d无法匹配"abc",所以函数会停止,返回0(因为没有任何项被成功赋值)。
基本用法示例
下面我们通过几个从简单到复杂的例子来理解 fscanf。
示例 1:读取单个整数
假设我们有一个文件 numbers.txt如下:
100
代码:
#include <stdio.h>
int main() {
FILE *fp;
int number;
// 以只读模式打开文件
fp = fopen("numbers.txt", "r");
if (fp == NULL) {
perror("无法打开文件");
return 1;
}
// 从文件中读取一个整数,并存入 number 变量
int result = fscanf(fp, "%d", &number);
if (result == 1) {
printf("成功从文件读取到整数: %d\n", number);
} else {
printf("读取失败或文件格式不正确,\n");
}
// 关闭文件
fclose(fp);
return 0;
}
输出:
成功从文件读取到整数: 100
示例 2:读取多个不同类型的数据
假设文件 data.txt 内容如下:
John Doe 25 95.5
代码:
#include <stdio.h>
#include <string.h> // 为了使用 strlen
int main() {
FILE *fp;
char first_name[50], last_name[50];
int age;
float score;
fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("无法打开文件");
return 1;
}
// 格式说明符中的空格会跳过输入中的任意空白字符
// %s 遇到空白字符会停止
int result = fscanf(fp, "%s %s %d %f", first_name, last_name, &age, &score);
if (result == 4) {
printf("姓名: %s %s\n", first_name, last_name);
printf("年龄: %d\n", age);
printf("分数: %.2f\n", score);
} else {
printf("读取失败或文件格式不正确,返回值: %d\n", result);
}
fclose(fp);
return 0;
}
输出:
姓名: John Doe
年龄: 25
分数: 95.50
示例 3:读取多行数据(使用循环)
这是 fscanf 最常见的用法之一:逐行处理文件。
假设文件 students.txt 内容如下:
Alice 20 88.0
Bob 21 92.5
Charlie 22 76.5
代码:
#include <stdio.h>
int main() {
FILE *fp;
char name[50];
int age;
float score;
fp = fopen("students.txt", "r");
if (fp == NULL) {
perror("无法打开文件");
return 1;
}
printf("学生信息列表:\n");
printf("--------------------\n");
// 循环读取,直到 fscanf 返回 EOF (即 -1)
while (fscanf(fp, "%s %d %f", name, &age, &score) == 3) {
printf("姓名: %-10s 年龄: %2d 分数: %5.1f\n", name, age, score);
}
printf("--------------------\n");
printf("文件读取完毕,\n");
fclose(fp);
return 0;
}
输出:
学生信息列表:
--------------------
姓名: Alice 年龄: 20 分数: 88.0
姓名: Bob 年龄: 21 分数: 92.5
姓名: Charlie 年龄: 22 分数: 76.5
--------------------
文件读取完毕。
重要注意事项和常见错误
-
文件指针必须有效:
- 在使用
fscanf之前,必须先用fopen成功打开文件,并检查其返回值是否为NULL,操作未打开或打开失败的文件是未定义行为。
- 在使用
-
参数必须是地址:
- 这是初学者最容易犯的错误,你必须传递变量的地址,而不是变量本身。
- 错误:
fscanf(fp, "%d", number); - 正确:
fscanf(fp, "%d", &number);
-
格式说明符与数据类型必须匹配:
- 格式字符串中的
%d必须对应一个int*,%f必须对应一个float*,%lf(用于double) 必须对应一个double*。 - 一个非常经典的错误是读取
double类型时使用%f:double my_double; // 错误:应该使用 %lf fscanf(fp, "%f", &my_double); // 可能导致读取不正确或警告 // 正确 fscanf(fp, "%lf", &my_double);
- 格式字符串中的
-
空白字符的处理:
- 在格式字符串中,一个或多个连续的空格、制表符(
\t)、换行符(\n)会匹配输入流中的任意数量的连续空白字符。 - 这使得
fscanf(fp, "%d %d", &a, &b);能够正确处理"10 20","10 20","10\n20"等多种情况。
- 在格式字符串中,一个或多个连续的空格、制表符(
-
%s的陷阱:%s格式说明符会读取连续的非空白字符,直到遇到下一个空白字符为止。- 它不会自动在读取的字符串末尾添加空字符
\0。fscanf会假设你提供的字符数组足够大,并会自动在末尾添加\0。 - 确保你的目标数组足够大,以防止缓冲区溢出,定义
char name[50];来存储一个名字,通常是比较安全的。
-
检查返回值:
- 总是检查
fscanf的返回值,这是判断操作是否成功以及是否到达文件末尾的唯一可靠方法,不要盲目地假设数据一定存在。
- 总是检查
fscanf vs. fgets + sscanf
在读取结构化文本文件时,fscanf 虽然方便,但也有其局限性,如果一行中的数据格式错乱(比如本应是数字的地方是字母),fscanf 可能会“卡住”或跳过整行。
一个更稳健的替代方案是结合使用 fgets 和 sscanf:
fgets: 从文件中读取一整行文本到一个字符串中。sscanf: 从一个字符串中读取格式化数据。
这种方法的好处是,即使一行数据格式错误,你的程序也能继续处理下一行,而不是轻易崩溃。
示例对比:
假设文件 bad_data.txt 内容如下:
Alice 20 90.0
Bob is not a number 22 85.0
Charlie 23 95.0
使用 fscanf 的风险:
// ... (打开文件) ...
while (fscanf(fp, "%s %d %f", name, &age, &score) == 3) {
// 当读到 "Bob is not a number..." 这一行时
// fscanf 会尝试用 "%s" 读取 "Bob",然后发现 "is" 不是数字,匹配失败。
// 它可能会返回 1 (只成功读了 name),或者直接进入未定义状态。
// 循环可能会变得不可预测。
}
使用 fgets + sscanf 的稳健方法:
#include <stdio.h>
#include <string.h>
#define MAX_LINE_LENGTH 256
int main() {
FILE *fp;
char line[MAX_LINE_LENGTH];
char name[50];
int age;
float score;
fp = fopen("bad_data.txt", "r");
if (fp == NULL) {
perror("无法打开文件");
return 1;
}
while (fgets(line, MAX_LINE_LENGTH, fp) != NULL) {
// 尝试从刚刚读取的行中解析数据
int items_read = sscanf(line, "%s %d %f", name, &age, &score);
if (items_read == 3) {
printf("成功解析: 姓名: %s, 年龄: %d, 分数: %.1f\n", name, age, score);
} else {
printf("跳过格式错误的行: %s", line);
}
}
fclose(fp);
return 0;
}
输出:
成功解析: 姓名: Alice, 年龄: 20, 分数: 90.0
跳过格式错误的行: Bob is not a number 22 85.0
成功解析: 姓名: Charlie, 年龄: 23, 分数: 95.0
| 特性 | fscanf |
fgets + sscanf |
|---|---|---|
| 读取单位 | 逐项读取(根据格式) | 逐行读取 |
| 优点 | 代码简洁,直接 | 非常稳健,易于处理错误行 |
| 缺点 | 容易因格式错误而“卡住” | 代码稍长,需要两步操作 |
| 适用场景 | 数据格式非常规范、可靠,且追求代码简洁 | 需要处理可能包含错误或不规范格式的文件 |
fscanf 是一个功能强大且方便的函数,特别适合处理格式非常严格的文件,但在实际应用中,尤其是在处理用户输入或不可控来源的文件时,fgets + sscanf 的组合通常被认为是更安全、更稳健的选择。
