scanf 的基本工作原理
scanf 的全称是 scan formatted(格式化扫描),它的作用是从标准输入(通常是键盘)读取数据,并根据你提供的 格式字符串 来解释这些数据,然后将转换后的值存入你提供的变量地址中。

(图片来源网络,侵删)
其基本语法为:
int scanf(const char *format, ...);
- 返回值:
scanf返回成功匹配并赋值的变量的个数,如果发生错误(如读取到文件结尾 EOF),则返回EOF,如果第一个匹配就失败,则返回0。 format(格式字符串):这是控制scanf行为的核心,它由两部分组成:- 普通字符:
scanf会尝试从输入中完全匹配这些普通字符。 - 格式说明符:以 开头,用于指定要读取的数据类型和格式。
- 普通字符:
核心:格式说明符
格式说明符是控制 scanf 读取数据类型的关键,其基本结构为:
%[修饰符...][类型字符]
常用类型字符
| 类型字符 | 对应数据类型 | 说明 |
|---|---|---|
%d |
int |
读取一个十进制整数。 |
%i |
int |
读取一个整数,可以是十进制、八进制(前缀0)或十六进制(前缀0x或0X)。 |
%c |
char |
读取一个单个字符。注意:它会读取包括空格、换行在内的任何字符。 |
%s |
char * |
读取一个字符串,直到遇到空白字符(空格、制表符、换行符)为止。注意:它不会自动在末尾添加 \0,需要确保目标数组足够大。 |
%f |
float |
读取一个单精度浮点数。 |
%lf |
double |
非常重要:读取一个双精度浮点数。在 scanf 中,double 必须使用 %lf。 |
%u |
unsigned int |
读取一个无符号十进制整数。 |
%x / %X |
int |
读取一个十六进制整数(前缀0x可选)。 |
%o |
int |
读取一个八进制整数(前缀0可选)。 |
%hd |
short |
读取一个短整型。 |
%ld |
long |
读取一个长整型。 |
%lld |
long long |
读取一个长长整型。 |
修饰符
修饰符可以放在 和类型字符之间,用来改变 scanf 的行为。
| 修饰符 | 名称 | 作用 | 示例 |
|---|---|---|---|
l |
long | 用于整数 (%ld, %lx) 或双精度浮点数 (%lf)。 |
scanf("%lf", &d); |
h |
short | 用于短整型 (%hd, %hu)。 |
scanf("%hd", &s); |
w (数字) |
宽度 | 指定读取的最大字符数,对于 %s 和 %c 非常有用。 |
scanf("%10s", str); 最多读取10个字符。 |
| 赋值抑制符 | 表示读取数据,但不进行赋值,即丢弃该数据。 | scanf("%d %*d %d", &a, &c); 读取三个整数,但第二个被丢弃。 |
|
[] |
扫描集 | 定义一个字符集合,scanf 会读取并匹配集合中的字符。 |
scanf("%[aeiou]", str); 读取所有元音字母。 |
scanf 的控制技巧与最佳实践
必须使用变量地址
scanf 需要知道要把读到的数据存到哪里去,所以你必须传递变量的地址,而不是变量本身。

(图片来源网络,侵删)
int age;
// 错误:scanf("%d", age); // 传递的是值,不是地址
scanf("%d", &age); // 正确:传递的是地址
处理返回值
scanf 可能会失败,所以永远不要忽略它的返回值,这可以防止程序在输入不匹配时进入未定义状态。
int num;
printf("请输入一个整数: ");
int result = scanf("%d", &num);
if (result == 1) {
printf("成功读取: %d\n", num);
} else {
printf("输入无效!\n");
// 清除输入缓冲区中的错误数据,防止后续读取也失败
while (getchar() != '\n'); // 详见下文
}
处理多余的空白字符
scanf 在处理 %d, %f 等数字类型时,会自动跳过开头的空白字符(空格、制表符 \t、换行符 \n),这通常很方便。
当格式字符串中有普通字符时,问题就来了。
// 问题代码
int a, b;
printf("请输入两个整数,用逗号分隔: ");
scanf("%d,%d", &a, &b);
// 如果用户输入 "10, 20" (注意10后面有个空格)
// scanf 会匹配 '1', '0',然后遇到 ',' 匹配成功。
// 然后它读取 ' ' (空格),但格式字符串中 ',' 后面是 '%d',%d会跳过空格,所以能正常读取20。
// 看起来没问题。
// 但如果格式字符串是 "%d, %d" (注意逗号后有个空格)
// 用户输入 "10,20"
// scanf 匹配 '1','0',然后匹配 ',',成功。
// 然后它期望匹配一个空格,但输入流中是 '2',不匹配!
// scanf 立即返回,只成功读取了 a=10,b没有被赋值。
解决方案:

(图片来源网络,侵删)
- 不要依赖自动跳过:如果你想要严格的格式控制,最好在格式字符串中明确写出所有期望的字符。
%c的特殊处理:%c不会跳过空白字符,如果你想读取一个非空白字符,可以使用%c(注意前有一个空格)。char ch; printf("输入一个字符: "); scanf(" %c", &ch); // 这里的空格会跳过输入缓冲区中所有剩余的空白字符
使用 width 限制输入长度(防止缓冲区溢出)
这是 scanf 最常见的安全漏洞,如果用户输入的数据超过了你为变量分配的空间,就会导致缓冲区溢出,可能引发程序崩溃或安全漏洞。
char name[20]; // 只能容纳19个字符 + 1个 '\0'
// 危险!
scanf("%s", name); // 如果用户输入 "ThisIsAVeryLongName",就会溢出
// 安全!
scanf("%19s", name); // 最多读取19个字符,留一个位置给 '\0'
清除输入缓冲区
当 scanf 读取失败时,导致失败的那个“坏”数据会留在输入缓冲区中,如果下一次 scanf 还在等待输入,它会直接读取这个坏数据,导致再次失败,形成死循环。
int age;
char name[20];
printf("请输入年龄: ");
scanf("%d", &age); // 用户输入 "abc",scanf失败,age未被赋值,"abc"留在缓冲区
printf("请输入姓名: ");
scanf("%s", name); // scanf从缓冲区读取 "abc",赋给name,然后等待下一个输入
// 程序会继续运行,但结果不是我们想要的。
// 解决方案:在 `scanf` 失败后,清除整个输入缓冲区
// 方法:循环读取字符,直到遇到换行符或文件结尾
while (getchar() != '\n');
进阶技巧:扫描集 [ ]
扫描集是一个非常强大的工具,可以让你定义一个“合法字符”的集合。
%[set]:读取set集合中的所有字符,直到遇到一个不在set中的字符。%[^set]:读取不在set集合中的所有字符,直到遇到一个在set中的字符。^表示“非”。
// 1. 读取一行,直到遇到换行符
char line[100];
scanf("%[^\n]", line); // 读取所有非换行符的字符
// 这相当于一个简单的 gets(line),但更安全,因为它限制了长度。
// 2. 读取所有数字
char digits[100];
scanf("%[0123456789]", digits); // 或者更简洁地写成 "%[0-9]"
// 3. 读取所有字母
char letters[100];
scanf("%[a-zA-Z]", letters);
// 4. 读取一个C语言标识符(字母、数字、下划线,且不能以数字开头)
// 先读一个字母或下划线
char first_char;
scanf("%[a-zA-Z_]", &first_char);
// 再读剩余的字母、数字、下划线
char rest[100];
scanf("%[a-zA-Z0-9]", rest);
// ... 然后拼接起来
scanf 的替代品
由于 scanf 容易出错且不够灵活,在很多现代 C 程序中,人们更倾向于使用更安全、更强大的函数组合:
-
fgets+sscanf(推荐)fgets(buffer, size, stdin): 从标准输入读取一行,并确保不会溢出buffer,它会把换行符\n也读进来。sscanf(buffer, format, ...): 从字符串(buffer)中按照格式读取数据。
优点:
fgets首先保证了输入的安全性。- 你可以检查
fgets的返回值来判断是否成功读取了一行。 - 你可以对同一行数据进行多次解析,非常灵活。
char buffer[100]; int a, b; printf("请输入两个整数,用空格分隔: "); if (fgets(buffer, sizeof(buffer), stdin)) { // 尝试从buffer中解析整数 if (sscanf(buffer, "%d %d", &a, &b) == 2) { printf("成功读取: a = %d, b = %d\n", a, b); } else { printf("输入格式错误!\n"); } } -
scanf的替代品:strtol,strtod等- 当你需要解析一个单独的数字或字符串时,这些函数比
scanf更安全、更可控,它们会告诉你转换了多少字符,以及是否有无效字符。
- 当你需要解析一个单独的数字或字符串时,这些函数比
总结表格
| 控制目标 | scanf 方法 |
优点 | 缺点 |
|---|---|---|---|
| 读取整数 | scanf("%d", &num); |
简单直接 | 容易因输入格式错误而卡住 |
| 读取浮点数 | scanf("%lf", &d); |
简单直接 | 必须用 %lf |
| 读取字符串(无空格) | scanf("%19s", str); |
简单 | 无法读取带空格的字符串 |
| 读取一行 | scanf("%[^\n]", line); |
代码简洁 | 不安全,无法限制长度;容易残留 \n 在缓冲区 |
| 读取一行(安全) | fgets(line, 100, stdin); |
安全,能限制长度 | 需要额外代码处理末尾的 \n |
| 读取特定字符集 | scanf("%[a-z]", chars); |
功能强大 | 语法不直观 |
| 丢弃输入 | scanf("%*d", &dummy); |
方便 | 可读性稍差 |
核心建议:
- 初学者:理解
scanf的基本用法和返回值检查,始终使用width限制字符串输入。 - 进阶开发者:强烈推荐使用
fgets+sscanf的组合,这种模式将“读取输入”和“解析输入”分离开,既保证了安全性,又提供了极大的灵活性,是处理用户输入的黄金标准。
