
scanf函数怎么用:格式说明符与常见示例
scanf 是 C 标准库中的格式化输入函数,常用于从标准输入读取整数、浮点数、字符和以空白分隔的文本。它上手简单,适合短小的交互式程序和结构固定的输入;但它不会自动解决所有输入校验和缓冲区安全问题。理解格式字符串、参数类型、返回值与输入边界,才能避免读错数据、覆盖内存或让程序陷入难以预期的状态。
一、基本用法与适用场景
声明 scanf 需要包含 <stdio.h>。它的声明形式如下:
int scanf(const char * restrict format, ...);
第一个参数是格式字符串,后续参数通常是变量的地址。格式字符串告诉函数要读取什么类型的数据;函数按照说明符解析输入,再把结果写入相应变量。
#include <stdio.h>
int main(void)
{
int age;
double height;
printf("请输入年龄和身高(米):");
if (scanf("%d %lf", &age, &height) != 2) {
fprintf(stderr, "输入格式不正确\n");
return 1;
}
printf("年龄:%d,身高:%.2f 米\n", age, height);
return 0;
}
例如输入 20 1.75,格式字符串中的空格允许输入中出现一个或多个空白字符。上例中的 &age、&height 是变量地址,scanf 需要通过地址写入读取结果。除字符串数组等特殊情况外,遗漏取地址符是常见错误。
scanf 适合读取空白分隔的字段,比如“整数、浮点数、单词”这样的输入。它不擅长完整地读取一行并处理其中所有边界情况,也不适合未经校验地解析不可信输入。需要读取整行、报告详细错误,或严格控制数字范围时,通常应先用 fgets 读取,再用 strtol、strtod 等函数解析。
二、常用格式说明符
格式说明符以百分号 % 开始。每个需要存储结果的说明符,都必须对应一个类型正确、可写入的参数。下表列出常见写法:
| 说明符 | 读取内容 | 对应参数类型 |
|---|---|---|
%d | 十进制有符号整数 | int * |
%i | 按前缀判断进制的有符号整数 | int * |
%u | 无符号十进制整数 | unsigned int * |
%o | 无符号八进制整数 | unsigned int * |
%x、%X | 无符号十六进制整数 | unsigned int * |
%f、%e、%g | 浮点数 | float * |
%lf、%le、%lg | 双精度浮点数 | double * |
%c | 一个或多个字符 | char * |
%s | 非空白字符序列,并追加字符串结束符 | char * |
%[...] | 匹配指定字符集合中的字符 | char * |
%p | 指针值 | void ** |
%n | 到当前位置已读取的字符数 | 与长度修饰符对应的整数指针 |
读取浮点数时要特别注意:scanf("%f", &x) 对应 float,scanf("%lf", &y) 对应 double。这与 printf 的格式规则不同;在 printf 中,%f 用于输出经默认参数提升后的浮点值。
%d 与 %i 的区别是进制判断方式。例如,%d 按十进制读取;%i 可以根据输入前缀识别十进制、八进制或十六进制。因此,对普通十进制数字通常使用 %d,不要因为名称相似就随意替换。
三、格式字符串如何影响输入
空白字符与换行
格式字符串中的空格、制表符、换行符等空白字符,会匹配输入中的零个或多个空白字符。它并不要求输入里恰好出现与格式字符串相同数量的空格。比如:
int day, month, year;
scanf("%d %d %d", &day, &month, &year);
可以读取空格或换行分隔的三个整数。大多数转换(如 %d、%lf、%s)会先跳过输入中的前导空白;%c、%[... ] 和 %n 不会这样做。
因此,格式字符串末尾的空白可能产生不符合预期的交互行为。例如 scanf("%d\n", &n) 中的换行并非“只读掉一个回车”,而是继续匹配任意数量的空白,通常要等到后续出现非空白输入才会结束这部分匹配。读取数字后要读取字符时,也要留意数字后留在输入流里的换行符。
普通字符与百分号
格式字符串中的普通字符必须与输入对应位置的字符匹配。例如 scanf("%d,%d", &a, &b) 要求两个整数之间有逗号;输入 12,34 可以匹配,输入 12 34 则不能按该格式匹配成功。若要读取输入中的百分号,格式字符串应使用 %%。
字段宽度
在百分号和转换说明符之间可以指定最大字段宽度,如 %5d、%9s。宽度限制该转换最多读取多少个输入字符,不包括跳过的前导空白。对于 %s 和字符集合转换,这个限制尤其重要:它限制写入字符数量,但不会替程序确认目标数组足够大。
四、读取整数和浮点数的示例
下面的程序读取一个整数和一个双精度浮点数,并检查赋值数量。保存为 read_number.c 后,可用支持 C99 的编译器编译,例如 cc -std=c99 -Wall -Wextra read_number.c -o read_number。
#include <stdio.h>
int main(void)
{
int count;
double price;
printf("请输入数量和单价:");
int assigned = scanf("%d %lf", &count, &price);
if (assigned != 2) {
fprintf(stderr, "未能读取两个有效字段\n");
return 1;
}
printf("数量=%d,单价=%.2f\n", count, price);
return 0;
}
检查返回值是必要的,但它只能说明转换是否成功并完成赋值,不能替代业务校验。比如输入可能成功读出负数,而业务要求数量必须非负;程序还应单独检查 count >= 0 等条件。
五、读取字符、单词与指定字符集合
%c:读取字符
%c 默认不跳过空白,所以数字读取后紧接着使用 %c,读到的常常是尚未消费的换行符。若希望跳过前导空白,可在说明符前加一个空格:
#include <stdio.h>
int main(void)
{
int number;
char choice;
printf("请输入一个整数和一个选项字符:");
if (scanf("%d %c", &number, &choice) != 2) {
fprintf(stderr, "输入不完整\n");
return 1;
}
printf("整数=%d,选项=%c\n", number, choice);
return 0;
}
这里 %d 后的格式空格会匹配零个或多个空白字符,再由 %c 读取选项。%c 本身不会为字符补上字符串结束符;若使用 %3c,则需要准备至少三个字符的存储空间,并且函数不会在这三个字符后自动写入 '\0'。
%s:读取一个单词
%s 会跳过前导空白,随后读取连续的非空白字符,并在结果后追加字符串结束符 '\0'。如果目标数组容量为 N,最多只能让它写入 N - 1 个字符,因此应把宽度设为 N - 1。
#include <stdio.h>
int main(void)
{
char name[32];
printf("请输入一个不含空白的名字:");
if (scanf("%31s", name) != 1) {
fprintf(stderr, "没有读到名字\n");
return 1;
}
printf("你好,%s\n", name);
return 0;
}
name 是字符数组,传给 scanf 时会转换为指向首元素的指针,所以这里不写 &name。宽度 31 给结尾的空字符预留一个位置。若去掉宽度,过长输入可能导致数组越界写入。宽度只限制本次读取,不会自动消费剩余部分;过长单词的剩余字符仍留在输入流中,可能影响后续读取。
%[...]:读取字符集合
字符集合转换会读取属于指定集合的连续字符,不匹配时停止;默认不跳过前导空白。集合开头的 ^ 表示取反。例如 %[A-Z] 读取大写英文字母序列,%[^\n] 读取直到换行为止的字符。它与 %s 一样会在结果末尾追加 '\0',所以同样要使用宽度为数组容量减一。
#include <stdio.h>
int main(void)
{
char line[80];
printf("请输入一段不超过 79 字符的文本:");
if (scanf("%79[^\n]", line) != 1) {
fprintf(stderr, "没有读到文本\n");
return 1;
}
printf("读取到:%s\n", line);
return 0;
}
这个例子适合说明字符集合的规则,但它不是通用的整行读取方案:如果输入一开始就是换行,转换不会匹配任何字符;成功读取后,换行符仍留在输入流中。实际程序读取整行时通常优先考虑 fgets。
六、抑制赋值、长度修饰符和特殊说明符
在百分号后、宽度前使用 *,可以读取并匹配字段但不保存结果。它常用于跳过固定格式中的某些部分:
#include <stdio.h>
int main(void)
{
int hour, minute;
/* 输入形如 12:30 */
if (scanf("%*d:%d", &minute) == 1) {
printf("分钟字段:%d\n", minute);
}
(void)hour;
return 0;
}
上例中实际只保存了冒号之后的整数;为了避免无用变量,更简洁的版本可以删除 hour 相关声明和 (void)hour。抑制赋值的转换不计入 scanf 的返回值。
整数类型有长度修饰符。常见对应关系包括:%hd 对应 short *,%ld 对应 long *,%lld 对应 long long *;无符号版本分别使用 %hu、%lu、%llu。使用时必须与实际变量类型匹配。size_t 可使用标准提供的宏 SCNuPTR 不适用;读取 size_t 的标准格式宏是 SCNuMAX 也不适用。应直接使用 <inttypes.h> 中适合目标类型的格式宏,或按平台和类型选用对应说明符;不要凭感觉把类型名拼成格式字符串。
更准确地说,常见的无符号大小类型 size_t 可使用 <inttypes.h> 提供的 SCNuMAX 读取到 uintmax_t,再进行范围检查和转换;若直接读取 size_t,应使用与其底层类型匹配的格式说明符。C 标准并没有一个可直接用于 size_t * 的通用专用转换说明符。uintmax_t 对应的标准宏是 SCNuMAX,示例如下:
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
int main(void)
{
uintmax_t value;
if (scanf("%" SCNuMAX, &value) != 1) {
fprintf(stderr, "整数输入失败\n");
return 1;
}
printf("%" PRIuMAX "\n", value);
return 0;
}
%n 不读取一个数值字段,而是把到当前位置已经读取的字符数写入整数对象;它不增加返回值中的赋值计数。它通常并非日常输入所需,若格式字符串可能由外部提供,更不应随意允许其中出现 %n。
七、返回值与输入失败
scanf 返回成功赋值的项目数。被 * 抑制的转换和 %n 不计入该数。若在第一次成功赋值之前遇到输入结束或读取错误,函数返回 EOF;若遇到不符合格式的输入,则返回此前已经成功赋值的数量,可能是 0。
因此,读取两个字段时应检查是否返回 2,而不是只判断返回值是否为真。反复用同一个格式调用 scanf、却不处理匹配失败,也可能导致程序一直读到同一个不匹配字符,形成循环。失败后要根据程序需求终止、清理输入行或改用逐行读取;不能假设失败的字符已经从输入流中移除。
八、重要边界与安全问题
说明符与参数类型必须匹配
scanf("%d", &value) 要求 value 是 int。若传入 long *、double * 或其他不匹配的指针,行为未定义,可能造成错误结果或内存破坏。指针参数通常不会被编译器自动转换成正确的目标类型,格式字符串也不会替程序检查类型。启用编译器警告(例如 GCC 或 Clang 的 -Wall -Wextra)可以帮助发现一部分问题。
数字越界不能靠返回值检测
格式匹配成功,不代表数字一定在目标类型可表示的范围内。C 标准对某些无法由目标对象表示的转换结果规定了未定义行为。因此,不应把 scanf("%d", &value) 当作可靠的整数范围检查工具。对于可能过大或来自不可信来源的数值,较稳妥的做法是用 fgets 读取文本,再用 strtol 或 strtoull 转换,检查 errno、结束位置和业务范围。
固定宽度不等于完整输入校验
给 %s 和 %[...] 指定宽度,可以防止一次转换写入超过目标字符数组容量,但不能自动验证输入是否完整、是否符合业务规则,也不会丢弃超过宽度的尾部内容。格式宽度必须根据实际数组大小计算;不同目标数组容量不同,不宜复制一段固定格式后就假定处处安全。
scanf 不会替程序限制业务范围
即使数字转换成功,也要检查它是否满足程序的要求,例如数量是否非负、端口号是否在允许范围内、用户输入是否为空。对不可信输入,格式字符串不是安全策略;程序还要限制输入长度、验证字段内容,并处理格式错误与超长输入。
九、用 fgets 和 strtol 读取严格整数
下面的示例逐行读取输入,并检查转换是否成功、是否有多余字符以及数值是否超出 int 范围。与直接扫描整数相比,这种方式更适合需要明确拒绝非法文本的场景。
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char line[128];
char *end;
long value;
printf("请输入一个 int 范围内的整数:");
if (fgets(line, sizeof line, stdin) == NULL) {
fprintf(stderr, "读取输入失败\n");
return 1;
}
errno = 0;
value = strtol(line, &end, 10);
if (end == line) {
fprintf(stderr, "没有读到整数\n");
return 1;
}
if (errno == ERANGE || value < INT_MIN || value > INT_MAX) {
fprintf(stderr, "整数超出范围\n");
return 1;
}
while (*end == ' ' || *end == '\t' || *end == '\n' ||
*end == '\r' || *end == '\f' || *end == '\v') {
++end;
}
if (*end != '\0') {
fprintf(stderr, "整数后存在多余字符\n");
return 1;
}
printf("读取到:%ld\n", value);
return 0;
}
这个程序的输入缓冲区长度有限。如果一行超出缓冲区,fgets 可能只读到这一行的一部分;需要处理任意长度输入时,还应检测是否读到了换行,并决定如何继续读取或拒绝该行。逐行读取并不意味着边界问题自动消失,但它让程序有机会在转换之前检查文本。
十、常见错误速查
- 忘记传变量地址:
scanf("%d", value)不是向整数变量写值的正确方式,应传&value。 - 浮点说明符和指针类型不匹配:
%f配float *,%lf配double *。 - 用不带宽度的
%s读取数组:输入可能超过数组容量;应设置合适宽度,或使用逐行读取。 - 以为
%c会跳过换行:它会读取空白;需要跳过前导空白时用" %c"。 - 把读取单词当成读取整行:
%s在空白处停止,不会读取带空格的完整文本。 - 忽略返回值:输入失败时变量可能未被赋值,之后使用它会导致错误;每次读取都应检查期望的赋值数量。
- 认为成功转换代表数字在范围内:格式匹配和范围校验是两件事;处理可能越界的数字应采用更适合范围检查的解析流程。
- 用不受信任的字符串作为格式参数:格式字符串应由程序控制。用户文本若被当作格式字符串解释,可能触发不安全的格式转换。
结语
scanf 的核心用法是:在格式字符串中写明输入结构,为每个转换提供类型正确的地址,并检查返回值。读取整数、浮点数和简单字段时,它简洁实用;读取字符串时要控制宽度,读取字符时要留意空白,处理数字时要额外考虑范围和业务规则。遇到需要精确处理整行、超长输入或不可信数据的场景,先用 fgets 取得有界文本,再做显式解析与校验,通常更容易写出可预测的程序。



