fgets() 是否返回带有短缓冲区的 NULL 兼容?

Is fgets() returning NULL with a short buffer compliant?

提问人:chux - Reinstate Monica 提问时间:4/30/2014 最后编辑:Communitychux - Reinstate Monica 更新时间:9/10/2020 访问量:3449

问:

在单元测试中,一个包含 的函数,当缓冲区大小 .显然,这样的缓冲区大小是愚蠢的,但测试正在探索极端情况。fgets()n < 2

简化代码:

#include <error.h>
#include <stdio.h>

void test_fgets(char * restrict s, int n) {
  FILE *stream = stdin;
  s[0] = 42;
  printf("< s:%p n:%d stream:%p\n", s, n, stream);
  char *retval = fgets(s, n, stream);
  printf("> errno:%d feof:%d ferror:%d retval:%p s[0]:%d\n\n",
    errno, feof(stream), ferror(stream), retval, s[0]);
}

int main(void) {
  char s[100];
  test_fgets(s, sizeof s);  // Entered "123\n" and works as expected
  test_fgets(s, 1);         // fgets() --> NULL, feof() --> 0, ferror() --> 0
  test_fgets(s, 0);         // Same as above
  return 0;
}

令人惊讶的是,回报既不是也不是.fgets()NULLfeof()ferror()1

下面的 C 规范似乎对这种罕见的情况保持沉默。

问题:

  • 是否在不设置或不合规的情况下返回?NULLfeof()ferror()
  • 不同的结果是合规行为吗?
  • 如果 1 或小于 1,这有区别吗?n

平台:gcc 版本 4.5.3 目标:i686-pc-cygwin

以下是 C11 标准的摘要,重点介绍一下:

7.21.7.2 fgets 函数

fgets 函数读取的字符数最多比 n [...] 指定的字符数少 1 个

如果成功,fgets 函数返回 s。如果遇到文件末尾,并且没有将任何字符读入数组则数组的内容保持不变,并返回 null 指针。如果在操作过程中发生读取错误,则数组内容不确定,并返回 null 指针。

相关文章
如何使用 feof 和 ferror for fgets(C 中的 minishell) 在 C 语言中创建 shell 时遇到麻烦(Seg-Fault 和 ferror) fputs()、fgets()、ferror() 问题和 C++ 等效项
fgets()

的返回值


[编辑]对答案的评论

@Shafik Yaghmour 很好地提出了整个问题:由于 C 规范没有提到当它不读取任何数据或写入任何数据时该做什么 when(),因此它是未定义行为。因此,任何合理的响应都应该是可以接受的,例如 return 、不设置标志、不理会缓冲区。sn <= 0NULL

至于当 时会发生什么,@Oliver Matthews 的回答和 McNabb @Matt注释表明 C 规范缺乏清晰度,考虑到 的缓冲区。C 规范似乎倾向于 的缓冲区 should 返回缓冲区指针,但不够明确。n==1n == 1n == 1s[0] == '\0'

C 语言律师 fgets

评论

1赞 Oliver Matthews 4/30/2014
可能值得注意的是,引用 c11 规范可能是错误的 - 尽管它在 c11 中可能没有改变,但 gcc 4.5 不支持任何 c11......
1赞 Oliver Matthews 4/30/2014
此外,gcc 4.8 中的行为是不同的 - 返回 . 返回(s,1)errno:0 feof:0 ferror:0 retval:0x7fff183c87a0 s[0]:0(s,0)errno:0 feof:0 ferror:0 retval:(nil) s[0]:42
0赞 chux - Reinstate Monica 4/30/2014
@Oliver Matthews 1) 关于潜在错误报价的正确 - 我的是 C11 草案的简称。2) 是的,虽然引用是 C11 式的,但编译不是在 C11 中完成的。我认为 C99 - 稍后会检查。
0赞 Shahbaz 4/30/2014
@OliverMatthews,最好说包含 GCC 4.8 的最新发行版中包含的 glibc 中的行为是不同的。很有可能是 glibc 中的一个错误已修复。
1赞 Jonathan Leffler 8/17/2023
令我感兴趣的是,签名是 char *fgets(char * restrict s, int n, FILE * restrict stream); (另见 POSIX fgets()),大小为 an 而不是 .这意味着这可能是负面的,但我不确定如果是这样会发生什么。空字节没有空间,因此它不应该写入任何内容。返回 NULL 是合理的。POSIX可以设置,但没有提到,这是最合适的。fgets()intsize_tnerrnoEINVAL

答:

3赞 Oliver Matthews 4/30/2014 #1

TL的;DR:那个版本的 glibc 有一个 n=1 的错误,规范(可以说)对 n<1 有歧义;但我认为较新的glibc采取了最明智的选择。

所以,c99的规格基本是一样的。

行为是错误的。glibc 2.19 给出正确的输出 (, .test_fgets(s, 1)retval!=nulls[0]==null

实际上,行为是未定义的。它没有成功(您最多无法读取 -1 个字符),但它没有达到两个“返回 null”条件中的任何一个(EOF&0 读取;读取错误)。test_fgets(s,0)

然而,GCC 的行为可以说是正确的(将指针返回到不变的 s 也可以) - feof 没有设置,因为它没有命中 eof;ferror 未设置,因为没有读取错误。

我怀疑 gcc 中的逻辑(手头没有源代码)在顶部附近有一个“if n<=0 return null”。

[编辑:]

仔细想想,我实际上认为 glibc 的行为是它所能给出的最正确的回应:n=0

  • 没有 eof 读取,所以feof()==0
  • 没有读取,所以不会发生读取错误,所以ferror=0

现在至于返回值 - fgets 不能读取 -1 个字符(这是不可能的)。如果 fgets 返回传入的指针,则看起来像是成功的调用。 - 忽略这个极端情况,fgets 承诺返回一个以 null 结尾的字符串。如果在这种情况下没有,你就不能依赖它。但是 fgets 会将读入数组的最后一个字符之后的字符设置为 null。鉴于我们在此调用中读取了 -1 个字符(大约),这会使它将第 0 个字符设置为 null 吗?

所以,最明智的选择是返回(在我看来)。null

评论

1赞 M.M 4/30/2014
在 n1256 中,它说“如果成功,fgets 函数将返回 s”。但是,它没有定义什么是“成功”。在我看来,这似乎是标准的一个缺陷。
2赞 Oliver Matthews 4/30/2014
不相信这一点。我将“成功”读作能够执行它被定义为要执行的任务(即从 s 读取最多 n-1 个字符,null 终止结果)。
8赞 Shafik Yaghmour 4/30/2014 #2

在较新版本的 , 中,行为是不同的,因为 ,它返回表示成功,这并不是对 fgets 函数2 段的不合理解读,它说(在 C99 和 C11 中都是一样的,强调我的):glibcn == 1s7.19.7.2

char *fgets(char * restrict s, int n, FILE * 限制流);

fgets 函数从 stream 指向的流中读取最多比 n 指定的字符数少 1 个,进入 s 指向的数组中。没有额外的 字符在换行符(保留)之后或文件末尾之后读取。空字符在读入数组的最后一个字符之后立即写入。

不是很有用,但不违反标准中所说的任何内容,它将读取最多字符和 null 终止。因此,您看到的结果看起来像是在 的更高版本中修复的错误。它显然也不是第 3 段所述的文件结束或阅读错误:0glibc

[...]如果遇到文件末尾,并且没有将任何字符读入数组,则数组的内容保持不变,并返回 null 指针。如果在操作过程中发生读取错误,则数组内容不确定,并返回 null 指针。

至于最后一种情况,这看起来只是未定义的行为。C99 标准草案部分一致性2 段说(强调我的):n == 04.

如果违反了出现在约束之外的“应”或“不应”要求,则该行为未定义。在本国际标准中,未定义的行为以其他方式以“未定义的行为”一词或省略任何明确的行为定义来表示。这三者在侧重点上没有区别;它们都描述了“未定义的行为”。

C11中的措辞相同。最多无法读取 -1 个字符,它既不是文件结束也不是读取错误。因此,在这种情况下,我们没有明确定义行为。看起来像一个缺陷,但我找不到任何涵盖此缺陷的缺陷报告。

评论

2赞 chqrlie 1/31/2017
虽然我同意你的结论,即未定义的行为,但我认为最多阅读 -1 个字符并非不可能。不读取字符是读取最多任何负数字符的唯一方法。微妙的语义问题是:负数可以描述多个字符吗?如果不是,则未定义负值的行为,如果是,则还有另一个有趣的极端情况:。test_fgets(s, 0);test_fgets(s, INT_MIN);
0赞 Joshua 12/11/2020
@chqrlie:看起来它会通过指针算术下溢调用未定义的行为。 可能小于 。test_fgets(s, INT_MIN);sINT_MAX
3赞 chqrlie 2/1/2017 #3

C标准(C11 n1570草案)是这样规定的(一些强调我的):fgets()

7.21.7.2 fgets 函数

概要

   #include <stdio.h>
   char *fgets(char * restrict s, int n,
               FILE * restrict stream);

描述

该函数从指向 by 的流中读取最多少于 n 指定的字符数 1 到指向 by 的数组中。在换行符(保留)之后或文件末尾之后不会读取其他字符。空字符在读入数组的最后一个字符之后立即写入。fgetsstreams

返回

如果成功,该函数将返回。如果遇到文件末尾,并且没有将任何字符读入数组,则数组的内容保持不变,并返回 null 指针。如果在操作过程中发生读取错误,则数组内容不确定,并返回 null 指针。fgetss

该短语最多读数比 n 指定的字符数少 1 个,这不够精确。负数不能表示多个字符*,但表示没有字符最多读取 -1 个字符似乎是不可能的,因此标准未定义大小写,因此具有未定义的行为。0n <= 0

对于 ,被指定为最多读取 0 个字符,除非流无效或处于错误状态,否则它应该会成功。短语 A null character is immediately write after the last character read into the array 是模棱两可的,因为没有字符被读入数组,但将这种特殊情况解释为 meaning 是有意义的。规格提供相同的读数,但精度相同。同样,该行为没有明确定义,因此它是未定义的1.n = 1fgetss[0] = '\0';gets_s

的规范更精确,明确指定了 的情况,并附上了有用的语义。不幸的是,这种语义无法实现:snprintfn = 0fgets

7.21.6.5 snprintf函数

概要

#include <stdio.h>
int snprintf(char * restrict s, size_t n,
     const char * restrict format, ...);

描述

该函数等价于 ,只不过输出被写入数组(由参数指定)而不是流中。如果 n 为零,则不写入任何内容,并且 s 可能是 null 指针。否则,超出 st 的输出字符将被丢弃,而不是写入数组,并且 null 字符将写入实际写入数组的字符的末尾。如果复制发生在重叠的对象之间,则行为未定义。snprintffprintfsn-1

该规范还澄清了这种情况,并使其成为运行时约束冲突:get_s()n = 0

K.3.5.4.1 gets_s功能

概要

#define __STDC_WANT_LIB_EXT1__ 1
#include <stdio.h>
char *gets_s(char *s, rsize_t n);

运行时约束

s不应为 null 指针。 既不能等于零,也不能大于 。从 stdin 读取字符时,应发生换行符、文件末尾或读取错误。nRSIZE_MAXn-1

如果存在运行时约束冲突,则设置为 null 字符,并且从读取和丢弃字符,直到读取换行符,或者发生文件末尾或读取错误。s[0]stdin

描述

该函数从指向 by 的流中读取最多少于 指定的字符数 1 的字符数,进入指向 by 的数组中。在换行符(被丢弃)之后或文件末尾之后不会读取其他字符。丢弃的换行符不计入读取的字符数。空字符在读入数组的最后一个字符之后立即写入。gets_snstdins

如果遇到文件末尾,并且没有将任何字符读入数组,或者如果在操作过程中发生读取错误,则设置为 null 字符,而 的其他元素采用未指定的值。s[0]s

推荐做法

该函数允许正确编写的程序安全地处理输入行,因为输入行太长而无法存储在结果数组中。通常,这要求调用方注意结果数组中是否存在换行符。请考虑使用(以及基于换行符的任何必要处理)而不是 .fgetsfgetsfgetsgets_s

返回

如果成功,该函数将返回。如果存在运行时约束冲突,或者遇到文件末尾并且未将任何字符读入数组,或者如果在操作过程中发生读取错误,则返回 null 指针。gets_ss

您正在测试的 C 库似乎在这种情况下存在一个错误,该错误已在 glibc 的更高版本中修复。返回应该意味着某种失败条件(与成功相反):文件结束或读取错误。其他情况(如无效流或未打开读取的流)或多或少被明确描述为未定义的行为。NULL

和 的情况未定义。返回是一个明智的选择,但澄清标准中对 要求的描述将是有用的,就像 的情况一样。n = 0n < 0NULLfgets()n > 0gets_s

请注意,还有另一个规范问题:参数的类型应该是 instead of ,但这个函数最初是由 C 作者在发明之前指定的,并且在第一个 C 标准 (C89) 中保持不变。当时更改它被认为是不可接受的,因为他们试图标准化现有的用法:签名更改会在 C 库之间造成不一致,并破坏使用函数指针或未原型函数编写良好的现有代码。fgetsnsize_tintsize_t


1C 标准在第 2 段(共 4 段)中规定。一致性 如果违反了出现在约束或运行时约束之外的“应”或“不应”要求,则该行为是未定义的。在本国际标准中,未定义的行为以其他方式以“未定义的行为”一词或省略任何明确的行为定义来表示。这三者在侧重点上没有区别;它们都描述了“未定义的行为”。

评论

0赞 chux - Reinstate Monica 2/1/2017
“对于 n = 1,fgets 被指定为最多读取 0 个字符,除非流是......在错误条件下。 --> 不清楚是否参考错误指示器。如果上一次读取设置了错误指示器,则不会指定设置的指示器会影响后续读取的返回值 - 只有“如果在操作期间发生读取错误......”。OTOH 错误指示器也有其软绵绵的规格,探测这可能是另一个问题。
0赞 chqrlie 2/2/2017
@chux:我暗示的是:缓冲区没有空间容纳任何字符,因此不会尝试从流中读取字节,因此不会出现输入失败的可能性。但是,如果设置了错误或文件结束标志,则返回将与流的状态一致。否则,返回似乎是合理的。NULLs
0赞 supercat 2/8/2017
@chux:如果流处于任何读取都会失败的状态,我不认为在查看之前测试这样的条件并返回 NULL 应该被解释为违反标准,但也不应该查看,得出结论认为没有必要实际读取任何内容,并返回成功。nn
0赞 chux - Reinstate Monica 2/8/2017
@supercat 我同意你的结果,假设“处于任何(随后)读取都会失败的状态”。然而,不需要这样的错误状态 - 可能只是报告先前读取的累积错误结果 - 以奇偶校验错误为例 - 而不是影响未来读取的状态。的结果不会影响下一个输入。如果由于前一个输入错误而返回,则下一个可能返回无错误的结果。IAC,C 规范缺乏有关此行为的详细信息,当 .ferror()ferror()NULLfgets()fgets()n==1
0赞 supercat 2/8/2017
@chux:C 不要求存在任何方法可以使流进入这种状态,但允许流以某种方式进入这种状态的可能性。允许实现在读取零字节时以任意方式选择是报告成功还是失败,这是标准必须允许许多实现被视为合规的自由的一小部分(I/O 失败可能会导致控制台上弹出消息或产生其他此类影响)。