7.4 格式化输入(scanf 函数)
输入函数 scanf
对应于输出函数 printf
,它在与后者相反的方向上提供同样的转换功能
具有变长参数表的函数 scanf
的声明形式如下:
int scanf(char *format, ...)
scanf
函数从标准输入中读取字符序列,按照 format
中的格式说明对字符序列进行解释,并把结果保存到其余的参数中
格式参数 format
定义解析的规则,其它所有参数都必须是指针,用于指定经格式转换后的相应输入保存的位置
在命令行中运行时,C 程序在调用 scanf
函数时,如果标准输入中没有未读的值,则程序会阻塞,控制传递给命令行程序
命令行中输入非换行符的字符时,C 程序会继续阻塞
直到输入换行后,命令行程序才会把控制传递给 scanf
函数,将读入的一行字符序列放到 C 程序的标准输入
scanf
会将标准输入中的字节数组当做字符序列进行解析
如果标准输入中的字节数组解析完了,没有碰到不符合 format
的内容或 EOF
,并且 format
的内容没有匹配完
则 C 程序会再次在 scanf
中挂起(函数没有返回),控制会再次传递给命令行程序,以此类推
直到 format
的内容匹配完或遇到不匹配的值或 EOF
时,scanf
才会返回
返回时,标准输入中的读指针只移动到本次 format
匹配中的部分的后边,没有匹配中的部分仍然是未读状态
当再次调用 scanf
函数时会继续从之前剩下的部分读起
因此如果命令行输入一个不匹配的字符串,while (scanf("%lf", &v) != EOF)
会导致死循环
不断地因为不匹配而从函数返回,因为没有匹配,无法移动读指针,又因为每次标准输入都有未读的值,不会将控制传递给命令行程序
scanf
函数内部会逐个字符地循环format
根据循环到的字符来决定对标准输入的读操作、解析逻辑、对变长参数表的指针移动与赋值、返回策略
函数将返回成功匹配的输入项的个数,但每个匹配项不一定都真的进行了赋值
比如有 2 个匹配项,但是 scanf
的赋值参数只有一个指针 scanf("%lf %lf", &v)
此时输入 "2.1 3.2"
,虽然只有 &v
被赋值,但 scanf
的返回值仍然是 2
如果到达文件的结尾,该函数将返回 EOF
注意,返回 EOF
与 0
是不同的
0
表示下一个输入字符与格式串中的第一个格式说明不匹配
另外还有一个输入函数 sscanf
,它用于从一个字符串(而不是标准输入)中读取字符序列:
int sscanf(char *string, char *format, arg1, arg2, ...)
它按照格式参数 format
中规定的格式扫描字符串 string
并把结果分别保存到 arg1
、arg2
、… 这些参数中
这些参数必须是指针
格式串通常都包含转换说明,用于控制输入的转换
格式串可能包含下列部分:
- 空格或制表符,在处理过程中将被忽略
- 普通字符(不包括
%
),用于匹配输入流中下一个非空白符字符 - 转换说明,依次由一个
%
、一个可选的赋值禁止字符*
、一个可选的数值(指定最大字段宽度)、
一个可选的h
、l
或L
字符(指定目标对象的宽度)以及一个转换字符
转换说明控制下一个输入字段的转换
一般来说,转换结果存放在相应的参数指向的变量中
但是,如果转换说明中有赋值禁止字符 *
,则跳过该输入字段,不进行赋值
输入字段定义为一个不包括空白符的字符串,其边界定义为到下一个空白符或达到指定的字段宽度
因为换行符也属于空白符(空白符包括空格符、 横向制表符、换行符、回车符、纵向制表符以及换页符)
所以 scanf
函数遇到行边界不会停下来,会继续按照规则读取并解析输入
如果没有匹配换行符的 format
,则会简单跳过换行符,就像跳过其它空白符一样
转换字符指定对输入字段的解释
对应的参数必须是指针,这也是 C 语言通过值调用语义所要求的
转换字符 | 输入数据 | 参数类型 |
---|---|---|
d | 十进制整数 | int * |
i | 整数 | int * ,可以是八进制(以 0 开头)或十六进制(以 0x 或 0X 开头) |
o | 八进制整数(可以以 0 开头,也可以不以 0 开头) | int * |
u | 无符号十进制整数 | unsigned int * |
x | 十六进制整数(可以 0x 或 0X 开头,也可以不以 0x 或 0X 开头) | int * |
c | 字符 | char * ,将接下来的多个输入字符(默认为 1 个字符)存放到指定位置。该转换规范通常不跳过空白符。如果需要读入下一个非空白符,可以使用%1s |
s | 字符串(不加引号) | char * ,指向一个足以存放该字符串(还包括尾部的字符’\0’)的字符数组。字符串的末尾将被添加一个结束符’\0’ |
e, f, g | 浮点数,它可以包括正负号(可选)、小数点(可选)及指数部分(可选) | float * |
% | 字符 % | 不进行任何赋值操作 |
表 7-2 | ||
scanf 函数的基本转换说明 |
转换说明 d
、i
、o
、u
及 x
的前面可以加上字符 h
或 l
前缀 h
表明参数表的相应参数是一个指向 short
类型而非 int
类型的指针
前缀 l
表明参数表的相应参数是一个指向 long
类型的指针
类似地,转换说明 e
、f
和 g
的前面也可以加上前缀 l
,它表明参数表的相应参数是一个指向 double
类型而非 float
类型的指针
通过函数 scanf
执行输入转换来改写第 4 章中的简单计算器程序:
#include <stdio.h>
main() /* rudimentary calculator */
{
double sum, v;
sum = 0;
while (scanf("%lf", &v) == 1)
printf("\t%.2f\n", sum += v);
return 0;
}
读取包含 25 Dec 1988
日期格式的输入行:
int day, year;
char monthname[20];
scanf("%d %s %d", &day, monthname, &year);
因为数组名本身就是指针,所以,monthname
的前面没有取地址运算符 &
字符字面值也可以出现在 scanf
的格式串中,它们必须与输入中相同的字符匹配
因此可以使用下列 scanf
语句读入形如 mm/dd/yy
的日期数据:
int day, month, year;
scanf("%d/%d/%d", &month, &day, &year);
scanf
函数忽略格式串中的空格和制表符,此外,在读取输入值时,它将跳过空白符(空格、制表符、换行符等等)
如果要读取格式不固定的输入,最好每次读入一行,然后再用 sscanf
将合适的格式分离出来读入
例如,假定我们需要读取一些包含日期数据的输入行,日期的格式可能是上述任一种形式:
while (getline(line, sizeof(line)) > 0) {
if (sscanf(line, "%d %s %d", &day, monthname, &year) == 3)
printf("valid: %s\n", line); /* 25 Dec 1988 form */
else if (sscanf(line, "%d/%d/%d", &month, &day, &year) == 3)
printf("valid: %s\n", line); /* mm/dd/yy form */
else
printf("invalid: %s\n", line); /* invalid form */
}
scanf
函数可以和其它输入函数混合使用
无论调用哪个输入函数,下一个输入函数的调用将从 scanf
没有读取的第一个字符处开始读取数据
注意,scanf
和 sscanf
函数的所有参数都必须是指针
最常见的错误是将输入语句写成 scanf("%d", n);
正确的形式应该为:scanf("%d", &n);
编译器在编译时一般检测不到这类错误