我尝试在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()的这种行为是标准化的,依赖于实现还是其他什么?我不是问为什么循环终止或为什么它不终止。 我对这个标准行为是否感兴趣。
答案 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
。使用getc
和fgetc
,这很简单:他们会尝试阅读一个角色,他们要么得到一个,要么不做(如果不是,那就是&#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))
后立即生效,并且没有进行额外的旅行。
底线:道德是(道德是):
feof()
。ferror()
和scanf
来测试之前输入调用失败的原因。fscanf
和while((r = fscanf(stream,"%s",buffer)) == 1) {
printf("%s\n", buffer);
}
。我们可能还会注意:请注意文件没有以换行结尾!他们的行为可能会有不同的表现。
附录:这是编写循环的更好方法:
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
转换从未发生过,保持缓冲区不受影响。