让我们将程序简化为进行输入的调用:
scanf("%d", &size); // Statement 1
while((c = getchar()) != EOF){ // 2
scanf("%d", &p); // 3
scanf("%d", &q); // 4
}
这绝对不是正确的方法。稍后我们就会了解正确的用法。现在,我们只分析一下发生了什么。准确理解如何进行很重要scanf
作品。这%d
格式代码会使其首先跳过任何空白字符,然后读取可以转换为十进制整数的字符。最终将读取一些不属于十进制整数的字符;最有可能是换行符。因为格式字符串现已完成,所以刚刚读取的未使用的字符将被重新插入流中.
所以当调用getchar
被制成,getchar
将读取并返回终止整数的换行符。在循环内部,有两次调用scanf("%d")
,每个字符的行为如上所示:跳过空格(如果有),读取十进制整数,并将未使用的字符重新插入输入流中。
现在,假设您运行该程序并输入数字42
然后按 Enter 键,然后按 Ctrl-D 关闭输入流。
The 42
将由语句 1 读取,并且(如上所述)换行符将由语句 2 读取。因此,当执行语句 3 时,不再有数据可读取。因为在读取任何数字之前会发出文件结束信号,scanf
将返回EOF
。但是,代码没有测试返回值scanf
;继续到语句 4。
What should此时发生的是scanf
语句 4 中应立即返回EOF
而不尝试读取更多输入。这就是 C 标准所说的应该发生的事情,也是 Posix 所说的应该发生的事情。一旦流上发出文件结束信号,任何输入请求都应立即返回EOF
直到手动清除文件结束指示器。 (请参阅下面的标准报价。)
但是 glibc 不符合标准,原因我们暂时不会讨论。它尝试另一次读取。因此用户必须输入另一个 Ctrl-D,这将导致scanf
在语句 4 处返回EOF
。同样,该代码不会检查返回代码,因此它继续执行 while 循环并调用getchar
再次在声明 2 处。由于同样的错误,getchar
不会立即返回EOF
,而是尝试从终端读取字符。因此,用户现在必须键入第三个 Ctrl-D 才能导致getchar
回来EOF
。最后,代码检查返回码,然后 while 循环终止。
这就是对正在发生的事情的解释。现在,很容易看出代码中至少有一个错误:返回值scanf
从未被检查过。这不仅意味着EOF
错过了,也意味着输入错误被忽略。 (scanf
如果输入无法解析为整数,则会返回 0。)这很严重,因为如果scanf
无法成功匹配格式代码,相应参数的值为不明确的并且不得使用。
简而言之:始终检查返回值*scanf
。 (以及其他 I/O 库函数。)
但还有一个更微妙的错误,虽然在这种情况下影响不大,但一般来说可能很严重。读取的字符getchar
语句 2 中的内容被简单地丢弃,无论它是什么。通常它是空白,所以它被丢弃并不重要,但你实际上并不知道,因为该字符被丢弃。也许这是一个逗号。也许那是一封信。也许它是什么很重要。
依赖于这样的假设:无论字符被读取,这是不好的风格。getchar
at 语句 2 并不重要。如果您确实需要查看下一个字符,则应该将其重新插入输入流中,就像scanf
does:
while ((c = getchar()) != EOF) {
ungetc(c, stdin); /* Put c back into the input stream */
...
}
但实际上,那个测试根本不是你想要的。正如我们已经看到的,这是极不可能的getchar
将返回EOF
在此刻。 (有可能,但可能性很小)。更有可能的是getchar
将读取换行符,即使下一个scanf
会遇到文件结尾。所以根本没有必要偷看下一个角色;正确的解决方案是检查返回码scanf
,如上所述。
把它们放在一起,你真正想要的更像是:
/* No reason to use two scanf calls to read two consecutive numbers */
while ((count = scanf("%d%d", &p, &q)) == 2) {
/* Do something with p and q */
}
if (count != EOF) {
/* Invalid format. Issue an error message, at least */
}
/* Do whatever needs to be done at the end of input. */
最后,让我们检查一下 glibc 的行为。有一个非常长期存在的错误报告 https://sourceware.org/bugzilla/show_bug.cgi?id=1190由一个链接到回答OP中引用的问题 https://stackoverflow.com/a/19890073/1566221。如果您不厌其烦地阅读 bugzilla 线程中的最新帖子,您会发现一个链接glibc 开发者邮件列表上的讨论 http://sourceware.org/ml/libc-alpha/2012-09/msg00343.html.
我给个TL;DR版本吧,省去你数字考古的麻烦。从C99开始,标准就明确了EOF是“粘性的”。 §7.21.3/11 规定所有输入的执行就像连续字节被读取一样fgetc
:
...字节输入函数从流中读取字符,就像连续调用fgetc
功能。
§7.21.7.1/3 规定fgetc
回报EOF
如果设置了流的文件结束指示符,则立即:
如果设置了流的文件结束指示符,或者如果流位于文件结束处,则设置流的文件结束指示符,并且fgetc
函数返回EOF
。否则,fgetc
函数返回stream指向的输入流中的下一个字符。如果发生读取错误,则会设置流的错误指示符,并且fgetc
功能
回报EOF
.
因此,一旦设置了文件结束指示符,由于检测到文件结束或发生某些读取错误,后续的输入操作must立即返回EOF
而不尝试从流中读取。有多种方法可以清除文件结束指示器,包括clearerr
, seek
, and ungetc
;一旦文件结束指示器被清除,下一个输入函数调用将再次尝试从流中读取。
然而,情况并非总是如此。在C99之前,从已经返回的流中读取的结果EOF
未指定。不同的标准库选择以不同的方式处理它。
因此,我们决定不更改 glibc 以符合(当时的)新标准,而是保持与某些其他 C 库(尤其是 Solaris)的兼容性。 (错误报告中引用了 glibc 源代码中的注释。)
尽管有一个令人信服的论点(至少对我来说是令人信服的),即修复该错误不太可能破坏任何重要的东西,但仍然有一定程度的不愿意对此采取任何行动。因此,十年后,我们看到了一个仍然悬而未决的错误报告和一个不合格的实现。