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

时间:2018-06-17 10:39:34

标签: c scanf eof

我尝试在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()的这种行为是标准化的,依赖于实现还是其他什么?我不是问为什么循环终止或为什么它不终止。 我对这个标准行为是否感兴趣

3 个答案:

答案 0 :(得分:5)

根据我的经验,在使用<stdio.h>处理&#34; eof&#34;的精确语义时和&#34;错误&#34;比特是非常非常微妙的,以至于它通常不值得(甚至可能不可能)试图理解它们是如何工作的。 (关于SO的first question I ever asked就是这个,虽然它涉及C ++,而不是C。)

我想你知道这一点,但首先要明白的是feof()的意图非常 来预测下一次输入尝试是否会到达结束时文件。意图甚至不是说输入流是&#34; at&#34;文件的结尾。考虑feof()(以及相关的ferror())的正确方法是他们为错误恢复,告诉您更多关于为什么之前的输入呼叫失败。

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

但是您确切地询问fscanf何时点击文件结尾并设置eof位,而不是getc / fgetc。使用getcfgetc,这很简单:他们会尝试阅读一个角色,他们要么得到一个,要么不做(如果不是,那就是&#t} #39;或者是因为它们遇到了文件结尾或遇到了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); } } 说明符不仅在它遇到文件结尾或出错时停止,而且在它遇到空白字符时停止。 (这就是为什么人们在评论中询问您的输入文件是否以换行符结束。)

我已尝试过该计划

\n

这与您发布的内容非常接近。 (我在printf中添加了This is a test. ,以便更容易看到输出,并且更好地匹配输入。)然后我在输入上运行程序

This
is
a
test.
test.

,特别是,所有这四行都以换行符结束。毫不奇怪,产出

while(!feof(stream))

最后一行是重复的,因为当你写This\n is\n a\n test. 时会发生什么(通常)。

但后来我在输入

上尝试了
This
is
a
test.

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

fscanf

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

我认为这两种情况之间的区别在于,在第一种情况下,当输入包含换行符时,\n读取最后一行,读取最后一行fscanf,注意到它是&#39; s空格,并返回,但它没有命中EOF,因此没有设置EOF位。在第二种情况下,没有尾随换行符,feof()在读取最后一行时命中文件结尾,因此设置eof位,因此while()条件中的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还不是真的,这意味着我们将通过循环进行最后的,额外的,不必要的,第五次调用。但我们可以看到,在第五次旅行中,fscanf returned 1: This (eof: 0) fscanf returned 1: is (eof: 0) fscanf returned 1: a (eof: 0) fscanf returned 1: test. (eof: 1) 返回-1,表示(a)它没有按预期读取字符串,(b)它达到了EOF。

另一方面,如果我在不包含尾随换行符的输入上运行它,则输出如下:

feof

现在,fscanf在第四次调用while(!feof(stream))后立即生效,并且没有进行额外的旅行。

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

  1. 不要写feof()
  2. 仅使用ferror()scanf来测试之前输入调用失败的原因。
  3. 请检查fscanfwhile((r = fscanf(stream,"%s",buffer)) == 1) { printf("%s\n", buffer); }
  4. 的返回值

    我们可能还会注意:请注意文件没有以换行结尾!他们的行为可能会有不同的表现。

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

    feof()

    当你运行它时,它总是打印出它在输入中看到的字符串。它不会重复任何事情;根据最后一行是否在换行符中结束,它没有做任何显着不同的事情。并且 - 显着 - 它根本不需要(需要)调用%s

    脚注:在所有这些中,我忽略了%s * scanf读取字符串而不是行的事实。此外,如果buffer遇到的字符串大于接收它的{{1}}字符串,那么{{1}}往往表现得非常糟糕。

答案 1 :(得分:1)

两个循环都不正确:feof(f)仅在尝试读取文件末尾失败后才设置为。在您的代码中,您不会测试返回fgetc()的{​​{1}},也不会测试EOF返回fscanf()0

实际上EOF可以设置流的文件结束条件(如果它到达文件末尾),如果文件不包含尾随换行符,它会为fscanf()执行,而{{1}如果文件以换行符结尾,则不会设置此条件。 %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;
}

C标准根据对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 在文件末尾无法从流中读取字节时设置文件结束条件。

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

答案 2 :(得分:1)

如果我可以在这里提供全面答案的tl; dr,格式化输入会读取字符,直到它有理由停止。既然你说

  

该文件中的最后一个字符不是换行符

并且%s指令读取一个非空白字符的字符串,在它读取! World!之后必须读取另一个字符。没有一个,它点亮了。

在短语的末尾放置空格(空格,换行符等),printf将打印最后一个单词两次:一次因为它读取它,再次因为scanf在击中eof之前找不到要读取的字符串,所以%s转换从未发生过,保持缓冲区不受影响。