来自 K&R 第二版标准库的 fgets 实现

fgets implementation from the std library of K&R 2nd edition

我知道教科书中的代码片段仅用于演示目的,不应遵守生产标准,但 K&R 第 2 版第 164-165 页说:

fgets and fputs, here they are, copied from the standard library on our system:

char *fgets(char *s, int n, FILE *iop)
{
    register int c;
    register char *cs;
    
    cs = s;
    while (--n > 0 && (c = getc(iop)) != EOF)
        if ((*cs++ = c) == '\n')
            break;
    *cs = '[=10=]';
    return (c == EOF && cs == s) ? NULL : s;
}

为什么 return 语句不是 return (ferror(iop) || (c == EOF && cs == s)) ? NULL : s; 因为:

  1. ANSI C89 standard 说:

If a read error occurs during the operation, the array contents are indeterminate and a null pointer is returned.

  1. 即使是本书附录 B 中说明的标准库也是如此。来自第 247 页:

fgets returns s, or NULL if end of file or error occurs.

  1. K&R 在 fputs 实施中使用 ferror(iop),在同一页面上的 fgets 实施下方给出。

通过上面的实现,fgets 将 return s 即使读取某些字符后出现读取 错误 。也许这是一个疏忽或者我错过了什么?

您是正确的,已发布的函数 fgets 实现的行为不符合 C89 标准。出于同样的原因,它也不符合现代C11/C18标准。

已发布的 fgets 实现正确处理 end-of-file,如果未读取单个字符,则仅 returning NULL。但是,它不能正确处理流错误。根据您对 C89 标准的引用(在这方面与 C11/C18 标准相同),如果发生错误,该函数应始终 return NULL,无论字符数如何读。因此,fgets 的已发布实现以与流错误完全相同的方式处理 end-of-file 是错误的。

值得注意的是K&R的第二版是1988年的,也就是ANSI C89标准发布之前的版本。因此,该标准的确切措辞可能在本书第二版编写时尚未最终确定。

fgets 的发布实现也不符合附录 B 中引用的规范。假设函数 fgets 的行为应该如附录 B 中指定的那样,那么发布的实现fgets 正确处理错误,但不能正确处理 end-of-file。根据附录 B 的引用,当出现 end-of-file 时,函数应该总是 return NULL(即使已成功读取字符,这没有意义)。

还值得注意的是,使用语句

return (ferror(iop) || (c == EOF && cs == s)) ? NULL : s;

问题中的建议不会使函数 fgets 的实现完全符合 C89/C11/C18 标准。当“在操作期间”发生流错误时,该函数应该 return NULL。然而,当 ferror returns 非零时,可能无法判断错误是否发生在“操作期间”,即在函数 fgets 被调用之前是否已经设置了流的错误指示符叫。由于调用 fgets 之前发生的错误,可能已经设置了流的错误指示器,但由于 end-of-file(即不是由于流错误),所有后续流操作成功或失败.函数 fgets 也不允许在函数开始时简单地调用 clearerr 来区分这些情况,因为它必须在 [=52] 之前恢复流错误指示器的状态=]ing。在 C 标准库中无法设置流的错误指示器;它需要一个 implementation-specific 函数。查看 getc 的 return 值并不总是能够解决这种歧义,因为 EOF 的 return 值可能同时表示 end-of-file 或错误。

Why is the return statement not return (ferror(iop) || (c == EOF && cs == s)) ? NULL : s;

因为这也不是最好的选择。

return (c == EOF && cs == s) ? NULL : s; 很好地处理了由于 end-of-file 而导致的 EOF 的情况,但由于 输入错误 而无法处理 EOF 的情况。

ferror(iop) 刚刚发生输入错误时为真 之前发生输入错误时。由于输入错误,在 returning EOF 之后 fgetc() 可能 return 非 EOF。这不同于 feof(),后者是 sticky。一旦 EOF 由于 end-of-file 发生,feof() 继续 return EOF,除非清除 end-of-file 标志。

更好的选择是确保 EOF 刚刚发生,而不是使用不合格的 ferror()

 if (c == EOF) {
   if (feof(iop)) {
     if (cs == s) return NULL;
   } else {
     return NULL; // Input error just occurred.
   }
 }
 return s;

迂腐n

病理病例: 当 n <= 0 as *cs = '[=28=]' 写入超出其合法范围的 cs[] 时,以下内容会受到影响。 --nn == INT_MIN 时的问题。

while (--n > 0 && (c = getc(iop)) != EOF) // Is --n OK?
  if ((*cs++ = c) == '\n')
    break;
*cs = '[=11=]';  // Is n big enough

// Alternative
while (n > 1 && (c = getc(iop)) != EOF) {
  n--;
  *cs++ = c;
  if (c == '\n') {
    break;
  }
}   
if (n > 0) *cs = '[=11=]';

迂腐CHAR_MAX >= INT_MAX

注意:在少数机器上(一些旧图形处理器),returning EOF 可能是有效的 CHAR_MAX。未提供替代代码。

未初始化c

测试 c == EOF 是 UB,因为不确定 c 曾经设置过小的 n

最好初始化Cregister int c = 0;


更多:
What are all the reasons fgetc() might return EOF?.
Is fgets() returning NULL with a short buffer compliant?.