为什么多个EOF进入结束程序?

Why multiple EOF enters to end program?

试图理解我的代码的行为。我期待 Ctrl-D 导致程序打印数组并退出,但是它需要按 3 次,并在第二次按后进入 while 循环。

#include <stdio.h>
#include <stdlib.h>

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 = %d\n", p, q);
    }

    for(i = 0; i < size; ++i)
        printf("%d\n", *ptr + i);

    free(ptr);
    return 0;
}

我看了这里的post,但不是很明白。 How to end scanf by entering only one EOF

读完后,我期待第一个 Ctrl-D 清除缓冲区,然后我期待 c = getchar() 拿起第二个 Ctrl-D 并跳出。相反,第二个 Ctrl-D 进入循环并打印 p 和 q,第三个 Ctrl-D 退出。

下面的代码在第一个 Ctrl-D-

时就消失了,这让这变得更加混乱
#include <stdio.h>

main() {

    int c, nl;

    nl = 0;
    while((c = getchar()) != EOF)
        if (c == '\n')
            ++nl;
    printf("%d\n", nl);
}

如果您通过调试器 运行 它,您将获得更清晰的画面。这是事件的顺序。

  1. scanf("%d", &size); 被调用。
  2. 输入一个数字,然后输入 ENTER。这里的关键是 scanf 不会消耗 ENTER.
  3. 产生的 \n
  4. getchar 被调用。这会消耗 \n.
  5. scanf("%d", &p); 被调用。这会消耗第一个 ctrl-D。如果检查了 return 值,那么很明显发生了错误。
  6. scanf("%d", &q); 被调用。这消耗了第二个 ctrl-D.
  7. 循环返回顶部并调用 getchar。第三个 ctrl-D 然后导致 EOFgetchar 编辑 return,因此循环在该点中断。

我将把它作为练习留给你解释为什么第二个程序按预期运行。

让我们将程序拆分为执行输入的调用:

scanf("%d", &size);             // Statement 1
while((c = getchar()) != EOF){  //           2
    scanf("%d", &p);            //           3
    scanf("%d", &q);            //           4
}

那绝对不是要走的路;我们稍后会介绍正确的用法。现在,让我们分析一下发生了什么。准确理解 scanf 的工作原理很重要。 %d 格式代码导致它首先跳过任何空白字符,然后读取字符,只要字符可以变成十进制整数。最终将读取一些不属于十进制整数的字符;很可能是换行符。因为格式字符串现在已经完成,所以刚刚读取的未使用字符将重新插入到流中

因此,当调用 getchar 时,getchar 将读取 return 终止整数的换行符。在循环内部,然后有两次调用 scanf("%d"),每个调用的行为如上所示:跳过空格(如果有),读取十进制整数,并将未使用的字符重新插入输入流。

现在,假设您 运行 程序,输入数字 42,然后按回车键,然后按 Ctrl-D 关闭输入流。

42会被statement 1读取,而换行符(如前所述)会被statement 2读取。所以当statement 3执行时,就没有再读取数据了。因为在读取任何数字之前发出文件结束信号,所以 scanf 将 return EOF。但是,代码没有测试scanf的return值;它继续到声明 4.

此时应该发生的是语句 4 中的 scanf 应该立即 return EOF 而无需尝试读取更多输入.这就是 C 标准所说的应该发生的事情,也是 Posix 所说的应该发生的事情。一旦在流上发出文件结束信号,任何输入请求都应立即 return EOF 直到文件结束指示符被手动清除。 (标准引述见下文。)

但是 glibc,由于我们暂时不讨论的原因,不符合标准。它尝试另一次读取。因此用户必须输入另一个 Ctrl-D,这将导致语句 4 中的 scanf 变为 return EOF。同样,代码不检查 return 代码,因此它继续 while 循环并在语句 2 处再次调用 getchar。由于相同的错误,getchar 不会立即 return EOF,而是尝试从终端读取一个字符。因此,用户现在必须键入第三个 Ctrl-D 才能使 getchar 变为 return EOF。最后,代码检查了一个return代码,while循环终止。


这就是对正在发生的事情的解释。现在,很容易在代码中发现至少一个错误:从未检查 scanf 的 return 值。这不仅意味着 EOF 被遗漏了,还意味着输入错误被忽略了。 (如果输入无法解析为整数,scanf 将 returned 0。)这很严重,因为如果 scanf 无法成功匹配格式代码,相应参数的值未定义,不得使用。

简而言之:始终检查 *scanf 中的 return 值。 (以及其他 I/O 库函数。)

但是还有一个更微妙的错误,在这种情况下影响不大,但一般来说可能很严重。 getchar 在语句 2 中读取的字符被简单地丢弃,不管它是什么。通常它是空格,所以它被丢弃并不重要,但你实际上并不知道,因为字符被丢弃了。也许这是一个逗号。也许是一封信。也许重要的是它是什么。

假设语句 2 中 getchar 读取的任何字符都不重要,这是一种糟糕的风格。如果你真的需要查看下一个字符,你应该将它重新插入到输入流中,就像 scanf 所做的那样:

while ((c = getchar()) != EOF) {
  ungetc(c, stdin);  /* Put c back into the input stream */
  ...
}

但实际上,那个测试根本不是你想要的。正如我们已经看到的,此时 getchar 将 return EOF 的可能性极小。 (这是可能的,但可能性很小)。更有可能的是 getchar 将读取换行符,即使下一个 scanf 将遇到文件结尾。所以偷看下一个角色绝对没有意义;正确的解决方案是检查 scanf 的 return 代码,如上所述。

综合起来,您真正想要的是:

/* 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 的行为。有一个很long-standing bug report linked to by an answer to the question cited in the OP. If you take the trouble to read through to the most recent post in the bugzilla thread, you'll find a link to a discussion on the glibc developer mailing list.

我给TL;DR版本吧,省去你数字考古的麻烦。从C99开始,标准就明确了EOF是"sticky"。 §7.21.3/11 声明所有输入的执行就像连续字节被 fgetc:

读取一样

...The byte input functions read characters from the stream as if by successive calls to the fgetc function.

并且 §7.21.7.1/3 声明如果设置了流的文件结束指示符,fgetc returns EOF 立即:

If the end-of-file indicator for the stream is set, or if the stream is at end-of-file, the end-of-file indicator for the stream is set and the fgetc function returns EOF. Otherwise, the fgetc function returns the next character from the input stream pointed to by stream. If a read error occurs, the error indicator for the stream is set and the fgetc function returns EOF.

因此,一旦设置了文件结束指示器,由于检测到文件结束或发生读取错误,后续输入操作必须立即return EOF 未尝试从流中读取。有很多东西可以清除文件结尾指示符,包括 clearerrseekungetc;一旦文件结束指示符被清除,下一个输入函数调用将再次尝试从流中读取。

然而,它并不总是那样。在 C99 之前,从已经 returned EOF 的流中读取的结果是未指定的。不同的标准库选择以不同的方式处理它。

因此决定不更改 glibc 以符合(当时的)新标准,而是保持与某些其他 C 库(尤其是 Solaris)的兼容性。 (错误报告中引用了 glibc 源代码中的注释。)

虽然有一个令人信服的论点(至少对我来说是令人信服的),即修复错误不太可能破坏任何重要的东西,但仍然有一定的不愿对此做任何事情。因此,十年后的今天,我们仍然有一个未解决的错误报告和一个不符合标准的实现。

这里有不同的事情。

首先,当您在输入终端键入Ctrl-D时,tty驱动程序正在处理您的输入,将每个字符添加到缓冲区中并处理特殊字符.其中一个特殊字符 (Ctrl-D) 意味着 占用最后一个字符并使它们全部可供系统使用 。这会导致两件事发生:首先,Ctrl-D 字符从数据流中删除;其次,到目前为止输入的所有字符都可以通过进程系统调用 read(2) 获得。 getchar() 是一种缓冲库调用,可避免对每个字符进行一次读取,从而允许在缓冲区中存储 先前读取的 个字符。

这里的其他问题是系统在 posix 系统(以及所有 unix系统)。当您进行 read(2) 系统调用时, return 值是实际读取的字符数(或 -1 以防失败,但这与 EOF 无关,很快就会解释)。 并且系统通过returning 0 个字符 来标记文件结束条件。因此,操作系统将文件的结尾标记为 read(2) return 0 字节(如果您只按下 return 键,那将生成 \n出现在数据流中)。

这里搞砸的第三件事是 getchar(3) 函数的 return 值的类型。它没有 return 一个 char 值。由于 所有 可能的字节值都可能被 return 编辑为 getchar(3),因此不可能为 EOF 信号保留特殊值。该解决方案在很久很久以前(当 getchar(3) 设计时,即在 C 语言的第一个版本中采用,(参见 The C programming language by Brian Kernighan和 Denis Ritchie,第一版。)是使用 int 作为 return 值,以便能够 return 所有可能的字节值 (0..255) 加上一个额外的值,称为 EOFEOF 的确切值取决于实现,但通常定义为 -1(我认为即使是标准现在也指定它必须定义为 -1,但不是当然)

因此,为了使万物协同工作,EOF 是一个 int 常量,定义为允许程序员编写 while ((c = getchar()) != EOF)。您永远不会从终端获得 -1 作为数据值。系统总是通过使 read(2) 到 return 0 来标记文件条件的结束。接收到 Ctrl-D 的终端驱动程序只是将其从流中删除并使数据达到,但不包括(不同于 Ctrl-JCtrl-M,换行和进位 return, 分别在数据流中也被解释和输入为 \n)

所以,接下来的问题是:为什么通常需要两个(或更多)Ctrl-D 个字符来发送信号 eof?

是的,正如我所说的,内核只能使用 Ctrl-D(但不包括它),因此 read(2) 的结果可以是一个数字第一次不同于 0。但可以肯定的是,如果您按顺序输入 Ctrl-D 字符两次,在第一个字符之后,两个字符之间不会有更多字符,确保 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 作为一个普通字符(它的值为[=52=]4

当你读取一个文件时,通常这会读取很多完整的缓冲区,然后进行部分读取(输入小于缓冲区大小的字节数)和最终读取零字节,表示文件已完成结束了。

备注

根据某些 unice 中 tty 驱动程序的配置,eof 字符可以更改并具有不同的含义。 return 字符和 linefeed 字符也会发生这种情况。请参阅 termios(3) 手册页以获取有关此内容的详细文档。