深入理解scanf()
动机
有人发给了我这样一段代码
1 | int i=0; |
这段代码的本意是依次读入10个整数到数组中,但实际上输入10个整数后,无论你怎样按回车它都不会进入到下一个语句,除非你再输入一个任意的非空白字符。
空白字符
空格、制表符、换行符(创建新行)、回车符、换页符、垂直制表符称为“空白字符”,因为它们与打印页上的单词和行之间的空格一样都是起到方便阅读的作用。
标记由空白字符和其他标记分隔(划分边界),如运算符和标点。 在分析代码时,C 编译器将忽略空白字符,除非您将它们用作分隔符或者字符常量或字符串文本的组成部分。
使用空白字符可以让程序更易于阅读。 请注意,编译器也将注释视为空白。来源:微软《C语言参考》
{style=”note”}
所以,这是为什么?它看上去不该那样,对吗?
注意到,%d后面跟了一个空格。
分析
国内的教材大多非常粗浅,寥寥数语便讲解完这个函数。
下面我将和大家一起阅读POSIX对于scanf()的标准文档,希望从中能找到问题的答案。
我尽可能保证我的翻译是正确的。
名称
scanf - 转换格式化的输入
定义
1 |
|
...说明其参数数量未定。
解释
基本运作方式
- The scanf() function shall read from the standard input stream stdin.
- Each function reads bytes, interprets them according to a format, and stores the results in its arguments.
- Each expects, as arguments, a control string format described below, and a set of pointer arguments indicating where
the converted input should be stored.
- The result is undefined if there are insufficient arguments for the format.
- If the format is exhausted while arguments remain, the excess arguments shall be evaluated but otherwise ignored.
scanf()从标准输入(stdin)中读取字节, 根据格式解释它们,并将结果存储在对应变量中。参数应当包括一个格式化字符串(
原文:control string format|指代形参中的format),以及一组指针参数(这解释了为什么变量前面要加&运算符),转换后的输入被存入指代的位置。- 如果指针参数的数量少于格式化字符串中的转换说明数量,那么结果是undefined(换而言之,不知道会发生什么,具体取决于编译器)
- 如果指针参数的数量少多格式化字符串中的转换说明数量,那么多余的指针参数仍然会被求值,但是会被忽略掉。
- 但不管怎么样,我们应该养成好的习惯,确保指针参数数量与转换说明数量一致。
转换说明如何与变量对应
默认情况下(当你用%d这种转换说明的时候),转换说明按从左到右的顺序依次与后面的指针参数从左到右对应
换而言之,第n个转换说明对应第n个指针参数
在这种情况下
%会被替换为%n$n 是 [ 1 , 指针参数总数 ] 范围内的十进制整数。
例如
1
2
3 scanf("%d",&a);
等价于
scanf("%1$d",&a);这也为我们提供了一种手动指定顺序的方法,例如下面的语句:
1 scanf("%2$d %1$d",&a,&b);需要注意的是,这两种形式在单个格式化字符串中不能混用(想想看,为什么?如果混用会发生什么?)。
{style=”note”}
格式
格式通过字符串的形式来描述,也就是所谓的格式化字符串。
格式化字符串又由零条或多条指令构成,每条指令又由下面的元素之一构成:
- 一个或多个空格字符
- 普通字符(既不是
%也不是空格) - 转换说明
例如在表达式scanf("%d %d",&a,&b);中%d,,和 都是指令
空格字符
由一个或多个空格字符组成的指令应通过读取输入来执行,直到不能再有有效的输入能被读取,或直到第一个不是空格字符的字节,并且该字节保持未读取状态。
这解释了一开始的问题,由于%d后面还有一个空格,%d匹配了第十个输入的数字之后,由%d后的空格来匹配第十个数字之后的空格,直到读取到一个非空格字节为止。
然而我们的输入到第十个数字就结束了,后面什么都没有,导致程序一直停留在scanf。
这个时候我们再输入任意的非空格内容,并回车,scanf就成功匹配到了非空格字符,scanf将它放回缓冲区并进入下一语句。
换而言之我们多输入的那第十一个数并没有被丢弃,它仍然在缓冲区中,稍后我们可以用scanf直接读取到它,但是这一切已经和这一段代码无关了。
{style=”note”}
普通字符
从输入中读取下一个字节并与构成指令的字节进行比较如果比较显示它们不一样,则指令将失败,并且将这个字节放回缓冲区,这个字节和后续字节将保持未读取状态。
同样,如果文件结尾、编码错误或读取错误阻止了读取,指令将失败。
{style=”note”}
转换说明
每个转换说明都由
%或%n$开头,然后依次出现以下内容:
可选的赋值抑制符
*一个可选的非零十进制整数,它指定最大字段宽度
一个可选的分配分配(assignment-allocation)字符
m一个可选的长度修饰符,它指定接收对象的大小
一个转换说明符,用于指定要应用的转换类型
{style=”note”}
长度修饰符
长度修饰符 适用的转换说明符 含义 hh d、i、o、u、x、X、n signed char or unsigned char h d、i、o、u、x、X、n short or unsigned short l(ell) d、i、o、u、x、X、n long or unsigned long l(ell) a, A, e, E, f, F, g, G double l(ell) c、s、[ wchar_t ll (ell-ell) d、i、o、u、x、X、n long long or unsigned long long L a, A, e, E, f, F, g, G long double j d、i、o、u、x、X、n intmax_t or uintmax_t z d、i、o、u、x、X、n size_t t d、i、o、u、x、X、n ptrdiff_t
{style=”note”}
转换说明
转换说明符 含义 d 可选符号十进制整数 i 可选符号整数 o 可选符号八进制整数 u 无符号十进制整数 x 可选符号十六进制整数 a, e, f, g 可选带符号的浮点数、无穷大或 NaN s 字符串 c 由字段宽度指定的数字的字节序列 p 匹配实现定义的序列集 n 不消耗任何输入 C lc S ls % 解决%字符的匹配问题
{style=”note”}
返回值
- 成功完成后,函数应返回成功匹配和分配的输入项的数量
- 如果早期匹配失败,数字可以为零
- 如果输入在第一次转换完成之前结束,并且没有发生匹配故障,应返回EOF
- 如果在第一次转换完成之前发生错误,并且在未发生匹配故障的情况下,应返回EOF,并设置 errno 以指示错误
- 如果读取 发生错误时,应设置流的错误指示器
例子
1 | scanf(" %2d", &i); |
如果我们输入一堆空格加321加一堆空格,再换行,
程序实际所做的:
- 忽略前面的一堆空格,直到读取到3,并把它放回缓冲区
- 从缓冲区中拿出3再拿出2,这时已经达到最大字段宽度,不再继续读取
- 计算32的二进制值为0010 0000
- 计算表达式&i得到i的内存地址,然后将0010 0000存入。
- 此时缓冲区里还剩1加一堆空格加换行符
溢出问题
如果str在堆中申请的空间较小,使用scanf(“%s”,str)时,很容易发生溢出,这是 C 语言开发中最经常出现的 bug 之一,内存访问越界,而且不易排查。
导致的后果:
- 如果此空间未被分配,那么程序运行后看起来程序一切正常
- 如果此空间已被分配,那么就非法修改了其它处所写入的数据,再读这块空间便得不到原始的数据,导致程序运行起来很诡异
- 有些内存空间是系统预留,访问时程序崩溃
编程时需要自己多加注意。
为了一定程序避免解决这个问题,后面出现了 scanf 的衍生 scanf_s。