【问题标题】:How a stream error indicator affects following input code?流错误指示器如何影响以下输入代码?
【发布时间】:2019-04-15 19:02:03
【问题描述】:

每个流都有“一个错误指示器,记录是否发生了读/写错误”。

它通常由各种函数设置,通常很少:fgetc(), fflush(), fseek(), ...

通过各种功能清除:rewind(), clearerr(), fopen(), ...

int ferror(FILE *stream) 报告状态。

当且仅当为stream 设置错误指示符时,ferror 函数才返回非零值。


在这种情况下,肯定是刚刚发生了输入错误。

if (!ferror(istream)) {
  int ch = fgetc(istream);
  if (ch == EOF && ferror(istream)) {
    puts("Input error just occurred");
  }
}

深入探索fgetc()fgetc() 不会返回EOF,因为设置了错误指示器,而是因为“如果发生读取错误”或文件结尾相关的原因1。通常一旦发生错误(例如串行流上的奇偶校验错误),代码不会在不清除错误的情况下继续读取,但要考虑当它继续时会发生什么。

我看到了 8 种情况:错误指示符fgetc() 之前设置/清除,fgetc() 是否返回EOF,而后面的ferror() 可能为真或不是。

int e1 = !!ferror(istream);
int eof = fgetc(istream) == EOF;
int e2 = !!ferror(istream);

假设没有 UB,是 8 种可能中的 5 种,而不是 3 种意外的?尤其是设置了错误指示器的有效输入可能吗? 2支持>

e1 eof e2  
0  0   0   Normal reading of valid data    
0  0   1   Unexpected
0  1   0   End-of-file
0  1   1   Input error
1  0   0   Unexpected
1  0   1   Normal reading of valid data with error indicator set!
1  1   0   Unexpected
1  1   1   Input error or end-of-file

在输入操作之前设置错误指示器,事情变得复杂,事先清除它可以简化代码。然而,这可以防止错误指示器累积。

如果代码没有事先清除 错误指示器,并且想要检测输入的 是否有罕见的输入错误,那么测试 @ 似乎是有意义的987654341@ 而不是ferror() 来检测。

检查ferror() 是否可能具有误导性? 还是我错过了有关错误指示器的某些内容?

char buf[80];
for (int i=0; i<(80-1); i++) {
  int ch = fgetc(stdin);
  if (ch == EOF) {
    if (ferror(stdin)) {
      puts("Input error or (Prior input error and end of file occurred)");  // ambiguous
    } 
    if (feof(stdin)) { 
      puts("End of file occurred");
    } else {
      puts("Input error occurred");  // ferror() test not needed
      i = 0; // ignore prior input
    }
    break;
  }
  if (ch == '\n') break;
  buf[i++] = ch; 
}
buf[i] = 0; 

类似的问题

File operations with error indicator set 。这个未回答的问题集中在累积错误指示符而不测试fgetc() 返回值(答案冒险进入errno 并制作用户错误标志),而这个问题试图简单地消除fgetc() 的歧义。

fgetc(): Is it enough to just check EOF? 不解决在fgetc() 之前设置的错误指示器

类似的问题适用于输出和 I/O 流,但这个问题侧重于输入流。


1int fgetc(FILE *stream) 退货

如果设置了流的文件结束指示符,或者如果流处于文件结束位置,则设置流的文件结束指示符并且fgetc 函数返回@987654351 @。否则,fgetc 函数从stream 指向的输入流中返回下一个字符。如果发生读取错误,则设置流的错误指示符并且fgetc 函数返回EOF。 C11dr §7.21.7.1 2

2 案例 0-1-0、1-1-1。似乎UCHAR_MAX == UINT_MAX,一个unsigned char,可以返回并等同于EOF,而不是由于文件结束或输入错误。

【问题讨论】:

  • 您是在寻找基于标准的答案(可能是language-lawyer)还是实用的答案?
  • @NominalAnimal 更接近于基于标准,而不是仅仅实用,因为我试图预测 错误指示器 的奥秘来解决:当fgetc() 返回EOF 时,是由于到最近的错误、文件结尾、一些宽 unsigned char其他东西? LL 已添加。
  • FWIW,对于 glibc,它总是由于检测到新的文件结尾或错误情况,因为 fgetc()/getc()/getchar() 函数从不检查错误标志。
  • @NominalAnimal glibc 是否还会在现有文件结束标志上返回 EOF 而不仅仅是新的文件结束标志?
  • glibc fgetc 的行为在 2.28 版中已更改,请参阅 sourceware.org/ml/libc-alpha/2018-08/msg00003.htmlsourceware.org/bugzilla/show_bug.cgi?id=1190 。我们确实认为 7.21.7p2,3 规定了“粘性 EOF”——该更改被描述为“纠正 [ing] 一个长期存在的 C99 一致性错误”。 (但是,EOF 指示符和错误指示符是两个独立的位。)

标签: c error-handling language-lawyer


【解决方案1】:

我对标准的解读是,如果错误指示符已经在输入时设置在流上,它没有明确说明fgetc 允许返回非EOF 值,但它没有明确说明它也不能。我对 Nominal Animal 的观察表示同情(如果它被删除或转移到聊天中,我将从 cmets 提出他的回答;请允许我磨碎我的个人斧头,并观察将 cmets 视为“短暂”的政策是有害的并且应该被废除):

恕我直言,标准是低音认可:实际上没有必要让 EOF 具有粘性,但如果错误不具有粘性,则存在意外遗漏错误的真正风险。

但是,如果现有的实现都始终不将错误视为粘性,那么改变行为将很难向委员会推销。因此,我向社区征集测试:

以下是Nominal Animal's test program 的缩短版、非交互式版本。它只在读取错误后查看fgetc 的行为,而不是在 EOF 之后。它使用SIGALRM 来中断读取,而不是使用 control-C,因此您无需执行任何操作,只需运行它。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

static _Noreturn void
perror_exit (const char *msg)
{
  perror (msg);
  exit (1);
}

static void
handler (int unused)
{
}

int
main (void)
{
  struct sigaction sa;
  int pipefd[2];
  FILE *fp;
  int ch, pa;

  setvbuf (stdout, 0, _IOLBF, 0);

  sa.sa_handler = handler;
  sa.sa_flags = 0; /* DO interrupt blocking system calls */
  sigemptyset (&sa.sa_mask);
  if (sigaction (SIGALRM, &sa, 0))
    perror_exit ("sigaction");

  if (pipe (pipefd))
    perror_exit ("pipe");

  fp = fdopen (pipefd[0], "r");
  if (!fp)
    perror_exit ("fdopen");

  printf ("before fgetc 1, feof = %d ferror = %d\n",
          feof (fp), ferror (fp));

  alarm (1);
  ch = fgetc (fp);

  if (ch == EOF)
    printf ("after fgetc 1, ch = EOF feof = %d ferror = %d\n",
            feof (fp), ferror (fp));
  else
    printf ("after fgetc 1, ch = '%c' feof = %d ferror = %d\n",
            ch, feof (fp), ferror (fp));

  write (pipefd[1], "x", 1);
  alarm (1);
  ch = fgetc (fp);
  pa = alarm (0);

  printf ("after fgetc 2, alarm %s\n",
          pa ? "did not fire" : "fired");

  if (ch == EOF)
    printf ("after fgetc 2, ch = EOF feof = %d ferror = %d\n",
            feof (fp), ferror (fp));
  else
    printf ("after fgetc 2, ch = '%c' feof = %d ferror = %d\n",
            ch, feof (fp), ferror (fp));

  return 0;
}

在我目前能得到的所有 Unix 上,这个程序的输出与 John Bollinger 的观察结果是一致的

最感兴趣的情况,实际上是允许的:

1  0   1   Normal reading of valid data with error indicator set!

我特别想知道这个程序在其他基于 Linux 的 C 库(例如 musl、bionic)上运行时会打印什么;既不是 Linux 也不是 BSD 门的 Unix;和窗户。如果您有更奇特的东西,请也尝试一下。我正在标记这个帖子社区维基;请编辑它以添加测试结果。

对于存在unistd.h 并且signal.h 定义sigaction 的环境,任何符合C89 的编译器都应该可以接受测试程序,但C11 _Noreturn 关键字的一种使用仅用于消除警告。如果您的编译器抱怨_Noreturn,请使用-D_Noreturn= 进行编译;结果不会受到影响。如果您没有unistd.h,则测试程序将不会在您的环境中做任何有意义的事情。如果您没有sigaction,您也许可以调整程序以使用替代接口,但您需要说服SIGALRM 以某种方式中断阻塞的read

结果

before fgetc 1, feof = 0 ferror = 0
after fgetc 1, ch = EOF feof = 0 ferror = 1
after fgetc 2, alarm did not fire
after fgetc 2, ch = 'x' feof = 0 ferror = 1

("设置错误指示符正常读取有效数据")

  • 带有 glibc 2.27 的 Linux
  • NetBSD 7.1.2
  • FreeBSD 11.2-RELEASE-p4
  • macOS 10.14,clang-1000.10.44.2
  • macOS 10.14,gcc 8.2.0(自制)

.

before fgetc 1, feof = 0 ferror = 0
after fgetc 1, ch = EOF feof = 0 ferror = 1
after fgetc 2, alarm did not fire
after fgetc 2, ch = EOF feof = 0 ferror = 1

(“粘滞错误”行为:fgetc(fp) 立即返回 EOF,而不调用 read,当 ferror(fp) 在输入时为真)

  • 计划 9(编译器无法识别 _Noreturn)

.

【讨论】:

    【解决方案2】:

    假设没有 UB,这 8 个中的 5 个是可能的,而不是 3 个意外的? 尤其是设置了错误指示符后是否可以进行有效输入?

    具体谈到标准的规定,我倾向于同意你的分析:

    • 很少有指定函数来清除流的错误指示符,fgetc() 不是其中之一。更一般地说,它们都不是数据传输功能。因此,如果在将该流提交给fgetc() 以供读取之前为该流设置了错误指示符,那么在该函数返回时仍应设置该错误指示符,尽管有所有其他考虑。这涵盖了这些情况:*

      1  0   0   Unexpected
      1  1   0   Unexpected
      1  1   1   Input error or end-of-file
      

      它也涵盖了与错误指示器的预期值有关的这种情况,尽管它没有说明它是否真的会发生:

      1  0   1   Normal reading of valid data with error indicator set!
      
    • fgetc() 被指定为在任何指定在流上设置文件结束指示符的情况下返回EOF。因此,如果fgetc() 返回除EOF 之外的任何内容,那么它不会在该调用中设置流的错误(或文件结尾)指示符。这涵盖了这些情况:

      0  0   0   Normal reading of valid data    
      0  0   1   Unexpected
      

      另一方面,如果fgetc() 确实返回EOF,那么流的文件结束指示符或其错误指示符之后应该被设置。但是标准区分了这些情况,并指定用户可以通过feof()ferror() 函数来区分它们。这涵盖了这些情况:*

      0  1   0   End-of-file
      0  1   1   Input error
      
    • 最后,我同意fgetc() 的任何行为都不是以流错误指示器的初始状态为条件的。仅假设流最初未定位在其末尾,并且其文件结束指示符最初未设置,“fgetc 函数从流指向的输入流中返回下一个字符。”这表明,最感兴趣的情况实际上是允许的:

      1  0   1   Normal reading of valid data with error indicator set!
      

      然而,该案例在摘要中被允许并不意味着它可以在实践中被观察到。细节似乎未指定,我希望它们取决于为相关流提供服务的驱动程序的实现。完全有可能一旦遇到错误,驱动程序将继续在后续读取中报告错误,直到适当地重置,甚至更长时间。从 C 的角度来看,这将被解释为每次后续读取时发生的(附加)错误,并且语言规范中没有任何内容可以防止这种情况发生。甚至没有使用清除流错误指示器的功能之一。

    如果代码没有事先清除错误指示并且想要 检测一行输入是否有罕见的输入错误,这似乎使 感觉要测试 !feof() 而不是要检测 ferror()

    检查 ferror() 是否可能具有误导性? 还是我错过了有关错误指示器的某些内容?

    我同意,如果最初设置了流的错误指示符,则它的文件结束指示符未设置,并且使用fgetc() 读取它会返回EOF,那么ferror() 无法有效区分结束- 文件和错误情况,而 feof() 应该。

    另一方面,在遇到错误后是否可以有效地继续读取给定流取决于实现,也可能取决于特定情况。即使通过clearerr() 调用清除了错误指示符,这也适用,更不用说错误指示符是否没有清除了。


    * 虽然我同意在UCHAR_MAX &gt; INT_MAX 的情况下对于EOF 存在歧义,但我断言这只是这种实现存在问题的几个原因之一.因此,实际上,我认为这些实现完全是假设性的。

    【讨论】:

    • 对“遇到错误后有用地继续读取给定流”的额外兴趣来自检查输入函数的稳健性。示例:my_getline()。调用此类函数时,可能会或可能不会设置 错误指示符。当遇到先前的错误时,防止调用它不在函数的控制范围内——就像fgetc()。输入错误的检测似乎比c == EOF &amp;&amp; ferror(...)更好地依赖c == EOF &amp;&amp; !feof(stdin),所以我一直在寻找其他关于输入错误的好主意。
    • @chux,我同意在符合 C 的实现中执行 int c = fgetc(s);,而不知道流 s 的初始状态,测试通过 c == EOF &amp;&amp; !feof(s) 读取的错误是有效的,并且它比涉及ferror 的模拟更可靠。如果要改用ferror,则必须首先确保流的错误指示符清晰。我不知道有任何与这些完全不同的替代方案。
    【解决方案3】:

    根据 OP 在评论中的要求,这是一个非常粗略、非常简单的程序,用于探索 GNU C 库相对于 fgetc()ferror()feof() 的行为:

    #define  _POSIX_C_SOURCE  200809L
    #include <stdlib.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <string.h>
    #include <signal.h>
    #include <errno.h>
    
    static volatile sig_atomic_t  interrupted = 0;
    
    static void interrupt_handler(int signum)
    {
        interrupted = 1;
    }
    
    static int install_interrupt(const int signum)
    {
        struct sigaction  act;
    
        memset(&act, 0, sizeof act);
        sigemptyset(&act.sa_mask);
        act.sa_handler = interrupt_handler;
        act.sa_flags = 0;
        if (sigaction(signum, &act, NULL) == -1)
            return -1;
    
        return 0;
    }
    
    int main(void)
    {
        int  n, c;
    
        if (install_interrupt(SIGALRM)) {
            fprintf(stderr, "Cannot install SIGALRM handler: %s.\n", strerror(errno));
            return EXIT_FAILURE;
        }
    
        if (ferror(stdin)) {
            fprintf(stderr, "Standard input is already in error state.\n");
            return EXIT_FAILURE;
        }
        if (feof(stdin)) {
            fprintf(stderr, "Standard input is already in end-of-input state.\n");
            return EXIT_FAILURE;
        }
    
        fprintf(stderr, "Testing stream error state. Please wait.\n");
    
        alarm(1);
        c = fgetc(stdin);
        if (c != EOF) {
            fprintf(stderr, "Stream error state test failed.\n");
            return EXIT_FAILURE;
        }
    
        fprintf(stderr, "fgetc(stdin) returned EOF.\n");
        fprintf(stderr, "ferror(stdin) returns %d.\n", ferror(stdin));
        fprintf(stderr, "feof(stdin) returns %d.\n", feof(stdin));
    
        fprintf(stderr, "\n");
        fprintf(stderr, "Testing stream end-of-input state. Please press Ctrl+D.\n");
        c = fgetc(stdin);
        if (c != EOF) {
            fprintf(stderr, "fgetc() returned %d; EOF was expected.\n", c);
            return EXIT_FAILURE;
        }
        fprintf(stderr, "fgetc(stdin) returned EOF.\n");
        fprintf(stderr, "ferror(stdin) returns %d.\n", ferror(stdin));
        fprintf(stderr, "feof(stdin) returns %d.\n", feof(stdin));
        if (!ferror(stdin) || !feof(stdin)) {
            fprintf(stderr, "Expected error and end-of-file states; aborting.\n");
            return EXIT_FAILURE;
        }
    
        fprintf(stderr, "\n");
        fprintf(stderr, "Testing fgetc() when stream in error and end-of-file state.\n");
        fprintf(stderr, "Please type something, then press Enter.\n");
        n = 0;
        c = fgetc(stdin);
        while (c != EOF && c != '\n') {
            n++;
            c = fgetc(stdin);
        }
        if (c == EOF) {
            fprintf(stderr, "Further input is not possible.\n");
            return EXIT_FAILURE;
        } else
            fprintf(stderr, "Further input is possible: %d characters (including Enter) read.\n", n + 1);
    
        return EXIT_SUCCESS;
    }
    

    当我在Linux上编译并运行上面的程序时,程序会输出

    Testing stream error state. Please wait.
    fgetc(stdin) returned EOF.
    ferror(stdin) returns 1.
    feof(stdin) returns 0.
    

    错误状态是由信号传递中断fgetc(stdin) 调用引起的。如您所见,它确实导致ferror(stdin) 返回非零值。但请注意,feof(stdin) 返回 0。

    输出继续:

    Testing stream end-of-input state. Please press Ctrl+D.
    

    Ctrl+C 产生输出

    fgetc(stdin) returned EOF.
    ferror(stdin) returns 1.
    feof(stdin) returns 1.
    

    此时,标准输入处于错误和文件结束状态。输出继续:

    Testing fgetc() when stream in error and end-of-file state.
    Please type something, then press Enter.
    

    如果我们现在输入 O K Enter,我们得到

    Further input is possible: 3 characters (including Enter) read.
    

    这证明至少 GNU C 库实现根本不检查流错误或文件结束状态。它只会尝试读取更多数据(使用 POSIXy 系统中的底层 read() 操作)。

    【讨论】:

    • 我也发现了类似的结果,但倾向于认为它不符合标准。 (IOW,该版本中的一个错误)。我还注意到文件结束标志被 fgetc() 调用 5 键 "x\n"、Ctrl d、".\n" 序列和 '.' 键清除。
    • @chux:在 POSIXy 系统上,EOF 行为是有道理的,因为如果文件在我们读取到最后之后被附加,我们将想要获取附加的数据;并且由于没有cleareof() 函数,否则重新打开文件将是唯一的方法。无论这是否符合标准,我没有意见:我决定避开语言律师。 (如果它是 glibc 中的一个错误,那么修复它就是与那些认为自己比其他人更了解标准的人进行的不切实际的斗争。不值得努力,IMO。)
    • 回复:“没有cleareof()”。 clearerr(istream) 不足以代替重新打开文件吗? IAC,感谢您探索 C I/O 的各个角落。
    • @NominalAnimal 作为记录,如果你认为你在 glibc 中发现了一个错误,并且你不想与 Joseph 和/或 Paul Eggert 争论这个问题,你可以给我发电子邮件,我会做。 Ulrich 不再参与其中。
    • @NominalAnimal 谢谢。我会在周末深入研究。正如 John Bollinger 指出的那样,该标准不需要粘性 error 指标,但只需一个粘性 EOF 指标。我认为,_IO_EOF_SEEN 不能在不破坏 ABI 的情况下更改,但我们可能还可以做一些其他事情来达到同样的效果。
    【解决方案4】:

    您所说的一切似乎都是正确的,ch==EOF &amp;&amp; !feof(f) 是在不干扰错误累积的情况下检查新错误的正确方法。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-07-18
      • 1970-01-01
      • 2022-11-27
      • 2020-11-21
      • 1970-01-01
      • 2021-06-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多