`fgetc()` 为什么可能返回 `EOF`?

8
当发生文件结束或输入错误时,fgetc()函数确实返回EOF。这是否意味着没有更多的数据可用呢?
FILE *inf = ...;
int ch;
while ((ch = fgetc(inf)) != EOF) {
  ;
}
if (feof(inf)) puts("End-of-file");
else if (ferror(inf)) puts("Error");
else puts("???");

只用feof(), ferror()进行测试是否足够?

注意: 这里的EOF是一个宏,其值为一些负数int,通常为-1。 它不是 end-of-file的同义词。

我发现一些问题和更多问题与这个问题相近,但没有列举所有可能性的问题。


我期待其他答案(不仅仅是我自己在“回答自己的问题”中)。 - chux - Reinstate Monica
此外,我的理解是当读取到 eof 时 feof()ferror() 的补充,但这可能是一个有缺陷的假设。 - Neil
1
@Neil,John Bollinger 对一个相关问题的回答可能会有所帮助。 - chux - Reinstate Monica
3个回答

6
这就是全部吗?这是否意味着没有更多的数据可用?
不,有更多方法可以出现"EOF"。EOF并不一定意味着没有更多数据-这取决于情况。
C语言库列举了三种情况,当`fgetc()`返回`EOF`时:
- 如果流的文件结束指示器已经被设置或者流已经在文件结束位置,那么流的文件结束指示器被设置,`fgetc`函数将返回`EOF`。 - 否则,`fgetc`函数将返回由指向流的输入流的下一个字符。 - 如果发生读取错误,则设置该流的错误指示器并返回`EOF`。
请注意,每个流(例如`stdin`)都有一个文件结束指示器和错误指示器。
  • stream just encountered the end-of-file

    (Most common) An attempt has been made to get more data, but there was none.

  • end-of-file indicator for the stream is set

    The stream first examines its end-of-file indicator. If it sees that the indicator is set, it returns EOF. No attempt is made to see if more data exists. Some types of streams will report EOF, but data will have arrived after the prior EOF report. Until the end-of-file indicator is cleared as with clearerr(), the return remains EOF. Example 1. Example 2.

  • Input error

    The stream error indicator is not examined. Yet the function failed for some reason to read data other than end-of-file. A common example is fputc(stdin). Often input errors are persistent. Some are not. More data may be available. The common strategy is to end the input.

      // Example where ferror() is true, yet fgetc() does not return EOF
      FILE *inf = stdin;
      printf("end-of-file:%d error:%d\n", feof(inf), ferror(inf));
      printf("fputc():%d\n", fputc('?', inf));  // EOF reported
      printf("end-of-file:%d error:%d\n", feof(inf), ferror(inf));
      printf("fgetc():%d\n", fgetc(inf));  // User typed in `A`, 'A' reported
      printf("end-of-file:%d error:%d\n", feof(inf), ferror(inf));
    

    Output

    end-of-file:0 error:0
    fputc():-1
    end-of-file:0 error:1
    fgetc():65
    end-of-file:0 error:1
    

    When ferror() is true, it does not mean the error just occurred, just sometime in the past.

其他情况

  • Apparent EOF due to improperly saving as char

    fgetc() returns an int with a value in the unsigned char range and EOF - a negative value.
    When fgetc() reads character code 255, yet saves that as a char on a system where char is signed, that commonly results in the char having the same value as EOF, yet end-of-file did not occur.

        FILE *f = fopen("t", "w");
        fputc(EOF & 255, f);
        fclose(f);
        f = fopen("t", "r");
        char ch = fgetc(f); // Should be int ch
        printf ("%d %d\n", ch == EOF, ch);
        printf("end-of-file:%d error:%d\n", feof(f), ferror(f));
        fclose(f);
    

    Output

    1 -1  // ch == EOF !
    end-of-file:0 error:0
    
  • Systems where UCHAR_MAX == UINT_MAX. Rare.

    (I have only come across this in some older graphics processors, still something C allows.) In that case, fgetc() may read an unsigned char outside the int range and so convert it to EOF on the function return. Thus fgetc() is returning a character code that happens to equal EOF. This is mostly an oddity in the C history. A way to mostly handle is:

      while ((ch = fgetc(inf)) != EOF && !feof(inf) && !ferror(inf)) {
        ;
      }
    

    Such pedantic code is rarely needed.

  • Undefined behavior

    Of course when UB occurs, anything is possible.

          FILE * f = fopen("Some_non_existent_file", "r");
          // Should have tested f == NULL here
          printf("%d\n", fgetc(f) == EOF); // Result may be 1
    

处理 fgetc() 返回值的一种稳健方法。

FILE *inf = ...;
if (inf) {  // Add test
  int ch; // USE int !

  // Pedantic considerations, usually can be ignored
  #if UCHAR_MAX > INT_MAX
    clearerr(inf); // Clear history of prior flags
    while ((ch = fgetc(inf)) != EOF && !feof(inf) && !ferror(inf)) {
      ;
    }
  #else
    while ((ch = fgetc(inf)) != EOF) {
      ;
    }
  #endif

  if (feof(inf)) puts("End-of-file");
  else puts("Error");

如果代码在读取到文件末尾或者发生错误后需要查找数据,可以调用 clearerr() 并重复执行 if() 代码块。

注意:[f]printf() 可能会重置 errno - wildplasser
@wildplasser 确实,“无论是否存在错误,库函数调用都可能将errno的值设置为非零值,只要在函数描述中未记录使用errno的情况...”,但这对此处的事情有何影响?errno不是文件的错误指示器。 - chux - Reinstate Monica
1
我建议添加这个(我的最爱):在Windows上以文本模式读取字节0x1a会设置EOF标志。 - HolyBlackCat
我认为这在MinGW和MSVC上都是适用的,所以“在Windows上”只是一个简写。 :) - HolyBlackCat
嗯,在 Wine 上它能工作。我认为它应该也能在实际的 Windows 上运行,但我要等到周一才能测试。 - HolyBlackCat
显示剩余4条评论

5
另一个EOF并不一定意味着'没有更多数据'的情况是读取磁带时。您可以在单个磁带上拥有多个文件,并在每个文件的末尾用EOF标记。当遇到EOF时,您可以使用clearerr(fp)来重置文件流上的EOF和错误状态,然后可以继续读取磁带上的下一个文件。但是,磁带(大部分)已经消失了,所以这几乎不再适用。

2
这也发生在终端上 -- 如果您指示EOF(通过按下系统特定的键组合,通常是ctrl-D或ctrl-Z),则从终端读取的程序将收到EOF。如果该程序随后使用clearerr,则可以从终端中读取更多内容。 - Chris Dodd
1
@ChrisDodd - 这取决于你在哪个平台上,或者你正在使用哪个版本的glibc。你说得对,在一些(可能是许多)Linux系统上,键入EOF指示符(通常为control-D)不会永久设置EOF指示符。在大多数其他Unix系统上,一旦你在终端上指示了EOF,你将继续获得EOF,直到你用clearerr()清除错误和EOF指示器。RHEL 7.4遭受了我认为是“这个bug”的折磨;macOS(我使用的所有版本)则没有。 - Jonathan Leffler
正确,但如果您调用clearerr,则可以继续从终端读取。 - Chris Dodd
如果你在你的系统上尝试这段代码:#include <stdio.h> int main(void) { for (int i = 0; i < 3; i++) { int c; while ((c = getchar()) != EOF) putchar(c); puts("EOF received"); } return 0; },你会发现许多(大多数)非 Linux 系统会接受第一个 EOF 并保持有效(因此你会立即得到三行 "EOF received"),但是很多 Linux 系统可以在不同输入集之间键入 EOF 指示符来接受多个输入。 - Jonathan Leffler
1
是的,如果您调用 clearerr(),在我所知道的所有系统上,您都可以继续从终端读取。 - Jonathan Leffler
2
磁带在商业环境中仍然被广泛使用,特别是在需要归档大量数据但不经常访问的情况下:仅就存储介质的每GB价格而言,磁带胜过硬盘。尽管如此,现在您可能会使用一些供应商提供的软件来访问磁带,而不是直接从C语言访问设备,因此您可能不再遇到这个问题了。请参阅:https://en.wikipedia.org/wiki/Linear_Tape-Open。 - Keiji

2

以下是一个不太常见的原因:

在Windows中,以文本模式读取字节0x1A会导致EOF。

这里所说的“Windows”包括MSVC和MinGW(所以这很可能是Microsoft CRT的怪癖)。但在Cygwin上不会发生这种情况。


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