KaranocaVe's Blog Help

深入理解scanf()

动机

有人发给了我这样一段代码

int i=0; do { scanf("%d ", &array[i]); sum[i] = sumdigits(array[i]); i++; } while (i < 10);

这段代码的本意是依次读入10个整数到数组中,但实际上输入10个整数后,无论你怎样按回车他都不会进入到下一个语句,除非你再输入一个任意的非空白字符。

所以,这是为什么?他看上去不该那样,对吗?
注意到,%d后面跟了一个空格。

分析

国内的教材大多非常粗浅,寥寥数语便讲解完这个函数。

下面我将和大家一起阅读POSIX对于scanf()的标准文档,希望从中能找到问题的答案。

我尽可能保证我的翻译是正确的。

名称

scanf - 转换格式化的输入

定义

#include <stdio.h> int scanf(const char *restrict format, ...);

...说明其参数数量未定。

解释

基本运作方式

  • scanf()从标准输入(stdin)中读取字节, 根据格式解释它们,并将结果存储在对应变量中。参数应当包括一个格式化字符串( 原文:control string format|指代形参中的format),以及一组指针参数(这解释了为什么变量前面要加&运算符),转换后的输入被存入指代的位置。

  • 如果指针参数的数量少于格式化字符串中的转换说明数量,那么结果是undefined(换而言之,不知道会发生什么,具体取决于编译器)

  • 如果指针参数的数量少多格式化字符串中的转换说明数量,那么多余的指针参数仍然会被求值,但是会被忽略掉。

  • 但不管怎么样,我们应该养成好的习惯,确保指针参数数量与转换说明数量一致。

格式

格式通过字符串的形式来描述,也就是所谓的格式化字符串。
格式化字符串又由零条或多条指令构成,每条指令又由下面的元素之一构成:

  • 一个或多个空格字符

  • 普通字符(既不是%也不是空格)

  • 转换说明

例如在表达式scanf("%d %d",&a,&b);%d, , 都是指令

返回值

  • 成功完成后,函数应返回成功匹配和分配的输入项的数量

  • 如果早期匹配失败,数字可以为零

  • 如果输入在第一次转换完成之前结束,并且没有发生匹配故障,应返回EOF

  • 如果在第一次转换完成之前发生错误,并且在未发生匹配故障的情况下,应返回EOF,并设置 errno 以指示错误

  • 如果读取 发生错误时,应设置流的错误指示器

例子

scanf(" %2d", &i);

如果我们输入一堆空格加321加一堆空格,再换行, 程序实际所做的:

  1. 忽略前面的一堆空格,直到读取到3,并把它放回缓冲区

  2. 从缓冲区中拿出3再拿出2,这时已经达到最大字段宽度,不再继续读取

  3. 计算32的二进制值为0010 0000

  4. 计算表达式&i得到i的内存地址,然后将0010 0000存入。

  5. 此时缓冲区里还剩1加一堆空格加换行符

溢出问题

如果str在堆中申请的空间较小,使用scanf(“%s”,str)时,很容易发生溢出,这是 C 语言开发中最经常出现的 bug 之一,内存访问越界,而且不易排查。

导致的后果:

  • 如果此空间未被分配,那么程序运行后看起来程序一切正常

  • 如果此空间已被分配,那么就非法修改了其它处所写入的数据,再读这块空间便得不到原始的数据,导致程序运行起来很诡异

  • 有些内存空间是系统预留,访问时程序奔溃

编程时需要自己多加注意。

为了一定程序避免解决这个问题,后面出现了 scanf 的衍生 scanf_s。

19 August 2024