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

may*_*aya 3 c scanf eof

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

while(!feof(stream))
{
    fscanf(stream,"%s",buffer);
    printf("%s",buffer);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

while (!feof(stream))
{
    c = fgetc(stream);
    printf("%c", c);
}
Run Code Online (Sandbox Code Playgroud)

fscanf()和fgetc()的这种行为是标准化的,依赖于实现还是其他什么?我不是问为什么循环终止或为什么它不终止.如果这是标准行为,我对这个问题很感兴趣.

Ste*_*mit 5

根据我的经验,当使用<stdio.h>"eof"和"error"位的精确语义时,非常非常微妙,以至于通常不值得(甚至可能不可能)尝试理解它们的确切方式工作.(我曾经问过第一个问题是关于这一点,虽然它涉及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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

This
is
a
test.
Run Code Online (Sandbox Code Playgroud)

而且,具体而言,所有这四行都以换行符结束.毫不奇怪,产量不足为奇

This
is
a
test.
test.
Run Code Online (Sandbox Code Playgroud)

最后一行重复,因为这是你写作时(通常)发生的事情while(!feof(stream)).

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

This\n
is\n
a\n
test.
Run Code Online (Sandbox Code Playgroud)

其中最后一行并没有有一个换行符.这一次,输出是

This
is
a
test.
Run Code Online (Sandbox Code Playgroud)

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

我认为这两种情况之间的区别在于,在第一种情况下,当输入包含换行符时,fscanf读取最后一行,读取最后一行\n,注意它是空白,并返回,但它没有命中EOF,因此没有设置EOF位.在第二种情况下,没有尾随换行符,fscanf在读取最后一行时命中文件结尾,因此设置eof位,因此feof()while()条件满足时,代码不会通过循环进行额外的跳转,最后一行不再重复.

如果我们看看fscanf回报值,我们可以更清楚地看到发生了什么.我像这样修改了循环:

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

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

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)
Run Code Online (Sandbox Code Playgroud)

我们可以清楚地看到,在第四次调用之后,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)
Run Code Online (Sandbox Code Playgroud)

现在,feof在第四次通话后立即生效fscanf,并且没有进行额外的旅行.

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

  1. 别写了while(!feof(stream)).
  2. 请使用feof()ferror()仅测试先前输入调用失败的原因.
  3. 做检查的返回值scanffscanf.

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


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

while((r = fscanf(stream,"%s",buffer)) == 1) {
    printf("%s\n", buffer);
}
Run Code Online (Sandbox Code Playgroud)

当你运行它时,它总是打印出它在输入中看到的字符串.它不重复任何事情; 根据最后一行是否以新行结尾,它不会做任何显着不同的事情.并且 - 显着 - 它根本不需要(需要)feof()!


脚注:在所有这些中,我忽略了%s使用*scanf读取字符串而不是行的事实.也是%s倾向于表现得非常糟糕,如果它遇到一个字符串,它比大buffer这是接受它.