试图了解我的代码的行为。 我期待Ctrl-D导致程序打印arrays并退出,但是需要3次按下,并在第二次按下后进入while循环。
#include #include void unyon(int p, int q); int connected(int p, int q); int main(int argc, char *argv[]) { int c, p, q, i, size, *ptr; scanf("%d", &size); ptr = malloc(size * sizeof(int)); while((c = getchar()) != EOF){ scanf("%d", &p); scanf("%d", &q); printf("p = %d, q = %dn", p, q); } for(i = 0; i < size; ++i) printf("%dn", *ptr + i); free(ptr); return 0; }
我在这里看了这篇文章,但我不太明白。 如何通过只输入一个EOF来结束scanf
读完之后,我期待第一个Ctrl-D清除缓冲区,然后我期待c = getchar()拿起第二个Ctrl-D然后跳出来。 而是第二个Ctrl-D进入循环并打印p和q,然后第三个Ctrl-D退出。
由于以下代码在第一个Ctrl-D-上退出,这使得更加困惑
#include main() { int c, nl; nl = 0; while((c = getchar()) != EOF) if (c == 'n') ++nl; printf("%dn", nl); }
让我们将程序剥离到输入的调用:
scanf("%d", &size); // Statement 1 while((c = getchar()) != EOF){ // 2 scanf("%d", &p); // 3 scanf("%d", &q); // 4 }
这绝对不是要走的路; 我们将在一点点得到正确的用法。 现在,让我们分析一下发生了什么。 了解scanf
工作原理非常重要。 %d
格式代码使它首先跳过任何空白字符,然后只要字符可以变成十进制整数就读取字符。 最终会读取一些不是十进制整数的字符; 最有可能是换行符。 由于格式字符串现在已完成,因此刚刚读取的未使用字符将重新插入到流中 。
因此,当调用getchar
, getchar
将读取并返回终止整数的换行符。 在循环内部,然后有两个scanf("%d")
调用scanf("%d")
,每个调用的行为如上所示:跳过空格(如果有),读取十进制整数,然后将未使用的字符重新插入输入流。
现在,让我们假设你运行程序,然后输入数字42
然后输入回车键,然后按Ctrl-D关闭输入流。
42
将由语句1读取,并且(如上所述)新行将由语句2读取。因此,当执行语句3时,不再有数据要读取。 由于在读取任何数字之前都会发出文件结尾信号,因此scanf
将返回EOF
。 但是,代码不测试scanf
的返回值; 它继续陈述4。
此时应该发生的是语句4中的scanf
应该立即返回EOF
而不尝试读取更多输入。 这就是C标准所说的应该发生的事情,也就是Posix所说的应该发生的事情。 一旦在流上发出文件结束信号,任何输入请求应立即返回EOF
直到手动清除文件结束指示符。 (参见下面的标准报价。)
但是glibc,由于我们不会进入的原因,不符合标准。 它试图再次阅读。 因此用户必须输入另一个Ctrl-D,这将导致语句4处的scanf
返回EOF
。 同样,代码不检查返回代码,因此它继续使用while循环并在语句2处再次调用getchar
。由于相同的错误, getchar
不会立即返回EOF
,而是尝试从终端读取字符。 因此,用户现在必须键入第三个Ctrl-D以使getchar
返回EOF
。 最后,代码检查返回码,while循环终止。
这就是对正在发生的事情的解释。 现在,很容易在代码中看到至少一个错误:从不检查scanf
的返回值。 这不仅意味着错过了EOF
,还意味着忽略输入错误。 (如果无法将输入解析为整数,则scanf
将返回0.)这很严重,因为如果scanf
无法成功匹配格式代码,则相应参数的值是未定义的 ,不得使用。
简而言之: 始终检查来自*scanf
返回值 。 (和其他I / O库函数。)
但是也有一个更微妙的错误,这在这种情况下几乎没有什么区别,但总的来说可能是严重的。 getchar
在语句2中读取的字符被简单地丢弃,无论它是什么。 通常它会是空格,所以它被丢弃并不重要,但你实际上并不知道它是因为字符被丢弃了。 也许这是一个逗号。 也许这是一封信。 也许这很重要。
依赖于语句2中getchar
读取的任何字符都是不重要的假设是不好的。 如果你真的需要查看下一个字符,你应该将它重新插入到输入流中,就像scanf
一样:
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的行为。 有一个非常长期的错误报告与OP中引用的问题的答案相关联。 如果您麻烦地阅读bugzilla线程中的最新post,您将找到glibc开发人员邮件列表上讨论的链接。
让我给出TL; DR版本,为您省去数字考古的麻烦。 自C99以来,标准已明确表明EOF是“粘性的”。 §7.21.3/ 11声明所有输入都被执行,好像fgetc
读取了连续的字节:
…字节输入函数从流中读取字符,就好像通过连续调用
fgetc
函数一样。
并且§7.21.7.1/ 3声明如果设置了流的文件结束指示符, fgetc
立即返回EOF
:
如果设置了流的文件结束指示符,或者流位于文件结尾,则设置流的文件结束指示符,并且
fgetc
函数返回EOF
。 否则,fgetc
函数返回stream指向的输入流中的下一个字符。 如果发生读取错误,则设置流的错误指示符,并且fgetc
函数返回EOF
。
因此,一旦设置了文件结束指示符,由于检测到文件末尾或发生了一些读取错误,后续输入操作必须立即返回EOF
而不尝试从流中读取。 各种各样的东西可以清除文件末尾的指示符,包括更清晰, clearerr
seek
和ungetc
; 一旦文件结束指示符被清除,下一个输入函数调用将再次尝试从流中读取。
然而,它并不总是那样。 在C99之前,从已经返回EOF
的流中读取的结果未指定。 不同的标准库选择以不同的方式处理它。
因此决定不改变glibc以符合(当时)新标准,而是保持与某些其他C库(尤其是Solaris)的兼容性。 (错误报告中引用了glibc源代码中的注释。)
虽然有一个令人信服的论点(至少对我来说很有说服力),修复bug不太可能打破任何重要的事情,但仍然有一些不愿意做任何事情。 因此,十年之后,我们将在这里发布一个仍然开放的错误报告和一个不合规的实现。
如果通过调试器运行它,您将获得更清晰的图像。 这是事件的顺序。
我将把它作为练习让你解释为什么第二个程序按预期运行。
这里有不同的东西搞乱。
首先,当你输入Ctrl-D
到输入终端时, tty驱动程序正在处理你的输入,在缓冲区中添加每个字符并处理特殊字符。 其中一个特殊字符( Ctrl-D
)表示占用最后一个字符并使它们全部可供系统使用 。 这使得有两件事情发生:首先,从数据流中删除Ctrl-D
字符; 第二,到目前为止输入的所有字符都可供进程系统调用read(2)
。 getchar()
是一个缓冲的库调用,它避免了每个字符读取一次,允许将先前读取的字符存储在缓冲区中。
这里搞乱的另一件事是系统在posix系统(以及所有unix系统)中发出文件结束信号的方式。 当你进行read(2)
系统调用时,返回值是读取的实际字符数(或者在失败时为-1
,但这与EOF
无关,很快就会解释)。 并且系统通过返回0
字符来标记文件结束条件 。 因此,操作系统标记文件的结尾使read(2)
返回0
字节(如果你只点击返回键,那将使n
出现在数据流中)。
这里弄乱的第三件事是来自getchar(3)
函数的返回值的类型。 它不返回char
值。 由于所有可能的字节值都可以为getchar(3)
返回,因此不可能为发出EOF
信号保留一个特殊值。 该解决方案采用了很长很久以前(当设计了getchar(3)
时,就是在C语言的第一个版本中,(请参阅Brian Kernighan和Denis Ritchie 的C编程语言 ,第一版)。一个int
作为返回值,能够返回所有可能的字节值( 0..255
)加上一个额外的值,称为EOF
的确切值取决于实现,但通常定义为-1
(我认为甚至标准现在指定它必须定义为-1
,但不确定)
因此,使所有事情协同工作, EOF
是一个定义的int
常量,允许程序员写入while ((c = getchar()) != EOF)
。 永远不会从终端获得-1
作为数据值。 系统始终通过使read(2)
返回0
来标记文件结束条件。 并且接收Ctrl-D
的终端驱动程序只是将它从流中删除并使数据达到,但不包括(与Ctrl-J
或Ctrl-M
,换行和进位返回,相应地,也被解释并且是在数据流中输入为n
)
那么,接下来的问题是: 为什么通常需要两个(或更多) Ctrl-D
字符来表示eof ?
是的,正如我所说,只有内核可以使用Ctrl-D
(但不包括它),因此read(2)
的结果可能是第一次不同于0
的数字。 但可以肯定的是,如果Ctrl-D
顺序输入两次Ctrl-D
char,则在第一次之后两个字符之间没有更多的字符,确保read()
零(字符串)。 通常,程序处于循环中,执行多次读取
while ((n_read = read(fd, buffer, sizeof buffer)) > 0) { /* NORMAL INPUT PROCESSING GOES HERE, for up to n_read bytes * stored in buffer */ } /* while */ if (n_read < 0) { /* ERROR PROCESSING GOES HERE */ } else { /* EOF PROCESSING GOES HERE */ } /* if */
在文件的情况下,行为是不同的,因为Ctrl-D
不被任何驱动程序解释(它存储在磁盘文件中)所以你将Ctrl-D
作为普通字符(它的值是