C99: fscanf() 设置eof 早于fgetc() 是否标准?

C99: Is it standard that fscanf() sets eof earlier than fgetc()?

我在 64 位 Windows PC 上尝试使用 VS2017(32 位版本),在我看来,fscanf() 在成功读取文件中的最后一项后立即设置 eof 标志。此循环在 fscanf() 读取文件中与流相关的最后一项后立即终止:

while(!feof(stream))
{
    fscanf(stream,"%s",buffer);
    printf("%s",buffer);
}

我知道这是不安全的代码...我只是想了解其行为。请原谅我;-)

在这里,stream 与包含 "Hello World!" 等字符串的普通文本文件相关。该文件中的最后一个字符 不是 换行符。

然而,处理完最后一个字符的 fgetc() 尝试在此循环中读取另一个字符,这导致 c=0xff (EOF):

while (!feof(stream))
{
    c = fgetc(stream);
    printf("%c", c);
}

fscanf() 和 fgetc() 的这种行为是标准化的、依赖于实现还是其他?我不是在问为什么循环终止或为什么不终止。 我对这是否是标准行为感兴趣

您的两个循环都不正确:feof(f) 仅在 尝试读取文件末尾未成功后设置。在您的代码中,您不测试 fgetc() returning EOF 也不测试 fscanf() returns 0EOF

确实,fscanf() 可以设置流到达文件末尾时的文件结束条件,如果文件不包含尾随换行符,%s 也会这样做,而 fgets() 如果文件以换行符结尾,则不会设置此条件。 fgetc() 仅在 returns EOF.

时设置条件

这是您的代码的修改版本,说明了此行为:

#include <stdio.h>

int main() {
    FILE *fp = stdin;
    char buf[100];
    char *p;
    int c, n, eof;

    for (;;) {
       c = fgetc(fp);
       eof = feof(fp);
       if (c == EOF) {
           printf("c=EOF, feof()=%d\n", eof);
           break;
       } else {
           printf("c=%d, feof()=%d\n", c, eof);
       }
    }

    rewind(fp); /* clears end-of-file and error indicators */
    for (;;) {
        n = fscanf(fp, "%99s", buf);
        eof = feof(fp);
        if (n == 1) {
            printf("fscanf() returned 1, buf=\"%s\", feof()=%d\n", buf, eof);
        } else {
            printf("fscanf() returned %d, feof()=%d\n", n, eof);
            break;
        }
    }

    rewind(fp); /* clears end-of-file and error indicators */
    for (;;) {
        p = fgets(buf, sizeof buf, fp);
        eof = feof(fp);
        if (p == buf) {
            printf("fgets() returned buf, buf=\"%s\", feof()=%d\n", buf, eof);
        } else
        if (p == NULL) {
            printf("fscanf() returned NULL, feof()=%d\n", eof);
            break;
        } else {
            printf("fscanf() returned %p, buf=%p, feof()=%d\n", (void*)p, (void*)buf, eof);
            break;
        }
    }
    return 0;
}

当 运行 标准输入从包含 Hello world 且没有尾随换行符 的文件重定向时,输出如下:

c=72, feof()=0
c=101, feof()=0
c=108, feof()=0
c=108, feof()=0
c=111, feof()=0
c=32, feof()=0
c=119, feof()=0
c=111, feof()=0
c=114, feof()=0
c=108, feof()=0
c=100, feof()=0
c=EOF, feof()=1
fscanf() returned 1, buf="Hello", feof()=0
fscanf() returned 1, buf="world", feof()=1
fscanf() returned -1, feof()=1
fgets() returned buf, buf="Hello world", feof()=1
fscanf() returned NULL, feof()=1

C 标准根据对 fgetc 的单独调用指定流函数的行为,fgetc 设置文件结束条件,当它无法从流的末尾读取字节时文件。

上面说明的行为符合标准并表明测试 feof() 不是验证输入操作的好方法。 feof() 在成功操作后可以 return 非零,在操作失败之前可以 return 0feof() 仅应用于在输入操作失败后区分文件结尾和输入错误。很少有程序会做出这种区分,因此 feof() 几乎从不故意使用,几乎总是表示编程错误。如需额外说明,请阅读:Why is “while ( !feof (file) )” always wrong?

根据我的经验,在使用 <stdio.h> 时,"eof" 和 "error" 位的精确语义非常非常微妙,以至于通常不值得这样做(它甚至可能是不可能的)试图准确地理解它们是如何工作的。 (SO 上的 first question I ever asked 就是关于这个的,尽管它涉及 C++,而不是 C。)

我想你知道这一点,但首先要了解的是 feof() 的意图非常 而不是 预测下一次输入尝试是否会到达文件末尾。其意图甚至不是说输入流是 "at" 文件的末尾。考虑 feof()(以及相关的 ferror())的正确方法是它们用于错误 恢复 ,以告诉您更多关于为什么以前输入调用失败。

这就是 writing a loop involving while(!feof(fp)) is always wrong.

的原因

但是您问的是 fscanf 到达文件末尾并设置 eof 位的确切时间,而不是 getc/fgetc。使用 getcfgetc,这很容易:他们尝试读取一个字符,他们要么读到一个,要么读不到(如果他们读不到,要么是因为他们读到了-文件或遇到 i/o 错误)。

但使用 fscanf 则更棘手,因为根据所解析的输入说明符,只有适合输入说明符的字符才会被接受。例如,%s 说明符不仅会在遇到文件末尾或出现错误时停止,还会在遇到空白字符时停止。 (这就是为什么人们在评论中询问您的输入文件是否以换行符结尾的原因。)

我已经试用了该程序

#include <stdio.h>

int main()
{
    char buffer[100];
    FILE *stream = stdin;

    while(!feof(stream)) {
        fscanf(stream,"%s",buffer);
        printf("%s\n",buffer);
    }
}

这与您发布的内容非常接近。 (我在 printf 中添加了一个 \n 以便输出更容易看到,并且更好地匹配输入。)然后我 运行 输入

上的程序
This
is
a
test.

并且,具体来说,所有四行都以换行符结尾。毫不奇怪,输出是

This
is
a
test.
test.

最后一行是重复的,因为当你写 while(!feof(stream)).

时(通常)会发生这种情况

但后来我在输入上试了一下

This\n
is\n
a\n
test.

最后一行没有换行。这次的输出是

This
is
a
test.

这一次,最后一行没有重复。 (输出仍然与输入不相同,因为输出包含四个换行符而输入包含三个。)

我认为这两种情况的区别在于,在第一种情况下,当输入包含换行符时,fscanf 读取最后一行,读取最后一行 \n,注意它是空格, 和 returns,但它没有达到 EOF,因此没有设置 EOF 位。在第二种情况下,没有尾随换行符,fscanf 在读取最后一行时到达文件末尾,因此设置了 eof 位,因此 while() 条件中的 feof() 是满足,并且代码不会在循环中进行额外的行程,并且不会重复最后一行。

如果我们查看 fscanf 的 return 值,我们可以更清楚地了解发生了什么。我这样修改了循环:

while(!feof(stream)) {
    int r = fscanf(stream,"%s",buffer);
    printf("fscanf returned %2d: %5s (eof: %d)\n", r, buffer, feof(stream));
}

现在,当我 运行 它在一个以换行符结尾的文件上时,输出是:

fscanf returned  1:  This (eof: 0)
fscanf returned  1:    is (eof: 0)
fscanf returned  1:     a (eof: 0)
fscanf returned  1: test. (eof: 0)
fscanf returned -1: test. (eof: 1)

我们可以清楚地看到,在第四次调用之后,feof(stream) 还不是真的,这意味着我们将在循环中进行最后一次、额外的、不必要的第五次循环。但是我们可以看到在第五次行程中,fscanf returns -1,表明 (a) 它没有按预期读取字符串,并且 (b) 它到达了 EOF。

如果我 运行 输入不包含尾随换行符,另一方面,输出是这样的:

fscanf returned  1:  This (eof: 0)
fscanf returned  1:    is (eof: 0)
fscanf returned  1:     a (eof: 0)
fscanf returned  1: test. (eof: 1)

现在,feof 在第四次调用 fscanf 后立即为真,并且不会进行额外的行程。

底线:道德是(道德是):

  1. 不要写while(!feof(stream))
  2. 请仅使用 feof()ferror() 来测试之前的输入调用失败的原因。
  3. 请检查 scanffscanf 的 return 值。

我们可能还会注意到:请注意文件未以换行符结尾!他们的行为可以出奇地不同。


附录:这是编写循环的更好方法:

while((r = fscanf(stream,"%s",buffer)) == 1) {
    printf("%s\n", buffer);
}

当您 运行 这样做时,它总是准确地打印它在输入中看到的字符串。它不重复任何东西;根据最后一行是否以换行符结尾,它不会做任何明显不同的事情。而且——重要的是——它根本不(需要)调用 feof()


脚注:在所有这一切中,我忽略了一个事实,即 %s 与 *scanf 读取 strings,而不是行。此外,如果 %s 遇到比要接收它的 buffer 更大的字符串,它的行为往往会非常糟糕。

如果我可以为此处的两个综合答案提供 tl;dr,格式化输入会读取字符,直到它有理由停止。既然你说

The last character in that file is not a newline character

%s 指令读取一串非白色 space 字符,在读取 World! 中的 ! 后,它必须读取另一个字符。没有一个,点亮eof。

将 whitespace(space,换行符,等等)放在短语的末尾,您的 printf 将打印最后一个单词两次:一次是因为它读取了它,另一次是因为scanf 在命中 eof 之前未能找到要读取的字符串,因此 %s 转换从未发生,缓冲区保持不变。