C99:fscanf()比fgetc()更早地设置eof是否是标准?

3

我在一台64位的Windows电脑上使用VS2017(32位版本)进行尝试,似乎fscanf()在成功读取文件中的最后一项之后立即设置了eof标志。当fscanf()读取与流相关的文件的最后一项后,该循环会立即终止:

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

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

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

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

while (!feof(stream))
{
    c = fgetc(stream);
    printf("%c", c);
}
和的这种行为是标准化的、依赖于实现的还是其他什么?我不是在问循环为什么终止或者为什么不终止。我对这个问题感兴趣的是它是否是标准行为。

1
这取决于fscanf是否读到了EOF。fscanf(“%c”)fgetc完全类似。 - Antti Haapala -- Слава Україні
3
EOF 不是 0xffEOF 被保证为负数,以便其与成功的 fgetc 返回值不会被混淆。 - melpomene
1
你需要展示一下你的“使用fscanf的类似循环”。我尝试了我认为你想要的方式,但在任何情况下都看到了相同的行为。 - Steve Summit
1
@maya 已经接近成功了,但是我尝试了三个程序中的一个 fscanf%s,对我来说它会打印最后一行两次,就像逐个字符版本会打印最后一个字符两次一样。你能描述一下你在你(现在)的第一个示例中如何打印输出,以及你的输入是什么样子的吗? - Steve Summit
1
@maya 不是这样的。%s 被指定为读取一系列连续的非空白字符,因此它必须继续读取直到遇到空白字符或 EOF。在后一种情况下,它可能会在流上设置 eof 指示器。 - melpomene
显示剩余25条评论
3个回答

5
根据我的经验,在使用<stdio.h>时,“eof”和“error”位的确切语义非常微妙,以至于通常不值得(甚至可能不可能)尝试精确理解它们的工作原理。(我在SO上提出的第一个问题就是关于此事,尽管涉及的是C ++而不是C。)
我认为你知道这一点,但首先要理解的是,feof()的意图绝对不是预测下一次输入尝试是否会到达文件结尾。实际上,其意图甚至不能说输入流“已经”到达了文件结尾。正确的思考feof()(以及相关的ferror())的方式是,它们用于错误恢复,以告诉您有关先前输入调用失败原因的更多信息。

这就是为什么使用while(!feof(fp))编写循环总是错误的原因。

但你现在问的是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,注意到它是空格符,然后返回,但它并没有遇到 EOF,所以没有设置 EOF 位。在第二种情况下,没有尾随换行符,fscanf 在读取最后一行时遇到了文件结束符,因此设置了 eof 位,因此 while() 中的 feof() 条件得到满足,代码不会再进行一次循环,最后一行也不会被重复。
如果我们查看 fscanf 的返回值,就可以更清楚地看到发生了什么。我将循环修改为以下形式:
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返回-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)

现在,在第四次调用fscanf后,feof立即变为true,并且不会再进行额外的操作。
底线是:道德是:
1. 不要写while(!feof(stream))。 2. 仅使用feof()和ferror()来测试为什么先前的输入调用失败。 3. 检查scanf和fscanf的返回值。
我们还需要注意:要小心不以换行符结尾的文件!它们的行为可能会有惊人的不同。
附录:这里有一种更好的编写循环的方法:
while((r = fscanf(stream,"%s",buffer)) == 1) {
    printf("%s\n", buffer);
}

当您运行此代码时,它总是准确地打印出输入中看到的字符串。它不会重复任何内容;它不会根据最后一行是否以换行符结尾而有任何显著不同的操作。并且 - 重要的是 - 它根本不需要调用 feof()
注:在这一切中,我忽略了使用*scanf读取字符串而不是行的事实。此外,如果遇到大于要接收它的缓冲区的字符串,%s倾向于表现得非常糟糕。

好文章!这个问题需要深入的解释。请注意,您对“%s”转换的描述没有涉及在单词前跳过空格字符的情况。如果存在这样的字符,但在读取它们后出现文件结尾,则C标准并不完全清楚文件结束指示器应该发生什么。同样,如果转换“%d”,那么在文件结束之前只有一个尾随“-”或“+”的流会发生什么? - chqrlie
@chqrlie 对的 - 现在回去读我的第一段话。 :-) - Steve Summit
是的,我也这样认为,fscanf()和它的伙伴们充满了怪癖和特殊情况,太微妙了以至于难以理解:即使实现了多次,我仍然会发现新的边角情况,而C标准并不完全清晰。 - chqrlie

1
你的两个循环都是错误的:feof(f)仅在尝试读取文件末尾后失败时才设置。在你的代码中,你没有测试fgetc()是否返回EOFfscanf()是否返回0EOF
实际上,如果fscanf()到达文件末尾,则可以设置流的文件结束条件,如果文件不包含尾随换行符,则对于%s,它会这样做,而fgets()则不会在文件以换行符结尾时设置此条件。fgetc()仅在返回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()可能返回非零值,在失败操作前,它可能返回0。feof()仅应用于区分无法成功输入操作后的文件结束和输入错误。很少有程序进行这种区分,因此feof()几乎从未被有意使用,几乎总是指示编程错误。如需额外解释,请阅读此文:为什么“while(!feof(file))”总是错误的?

feof() 非常偶尔且仅在某些情况下略微有用,即当您使用单独的 getc 或类似函数调用来读取一组相关字符(例如文件中的记录),而不是费力地测试每个返回值是否为 EOF 时,您可以在最后调用一次 feof(),如果它返回 true,则丢弃或使刚刚(您认为)读取的所有内容无效。 - Steve Summit
@SteveSummit:这很有趣,但风险和错误率很高:它只在最后一次读取操作是fgetc()(或getc()getchar())时才有效。大多数其他输入操作可能会设置文件结束条件,但仍然成功。如果您想使用此方法,请将最后一个fgetc()的返回值与EOF进行比较。 - chqrlie

1
如果我可以为这里的全面回答提供一个 tl;dr,那么格式化输入会读取字符,直到它有理由停止。由于你说文件中的最后一个字符不是换行符,并且 %s 指令读取一串非空格字符,在读取 World! 中的 ! 后,它必须再读取另一个字符。但是没有一个字符,它会触发 eof。在短语结尾加上空格(空格、换行符等),你的 printf 将会把最后一个单词打印两次:因为它读取了它,而且由于 scanf 在遇到 eof 前找不到要读取的字符串,所以 %s 转换从未发生,留下了缓冲区不变。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接