fgets()是否返回NULL并且符合短缓冲区?

在unit testing中,包含fgets()的函数在缓冲区大小n < 2时遇到意外结果。 显然这样的缓冲区大小是愚蠢的,但测试正在探索极端情况。

简化代码:

 #include  #include  void test_fgets(char * restrict s, int n) { FILE *stream = stdin; s[0] = 42; printf(" errno:%d feof:%d ferror:%d retval:%ps[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规范似乎对这种罕见的情况保持沉默。

问题:

  • 在没有设置feof()ferror()兼容行为的情况下返回NULL吗?
  • 不同的结果可能是合规行为吗?
  • 如果n为1或小于1,它会有所不同吗?

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

这是C11标准的摘要,一些强调我的:

7.21.7.2 fgetsfunction

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

如果成功, fgets函数返回s 。 如果遇到文件结尾没有字符读入数组,则数组的内容保持不变,并返回空指针。 如果在操作期间发生读取错误,则数组内容是不确定的,并返回空指针。

相关post
如何在fgets中使用feof和ferror(C中的minishell)
无法在C中创建shell(Seg-Fault和ferror)
fputs(),fgets(),ferror()问题和C ++等价物
fgets()的返回值


[编辑]评论答案

@Shafik Yaghmour很好地介绍了整个问题:由于C规范没有提到当它( n <= 0 )时它没有读取任何数据也没有向s任何数据时该怎么做,它是未定义的行为。 因此任何合理的响应都应该是可接受的,例如返回NULL ,设置无标志,单独保留缓冲区。

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

新版本的glibc的行为是不同的,对于n == 1 ,它返回s表示成功,这不是7.19.7.2 fgets函数段落2的不合理读数( 在C99和C11中都是相同的) ,强调我的 ):

char * fgets(char * restrict s, int n ,FILE * restrict stream);

fgets函数最多只读取一个小于n指定的字符数,该值由 stream指向的流指向s指向的数组。 在换行符(保留)或文件结束后不会读取其他字符。 在读入数组的最后一个字符后立即写入空字符。

不是非常有用但不违反标准中所述的任何内容,它最多会读取0字符并且无效终止。 因此,您看到的结果看起来像是在后来的glibc版本中修复的错误。 它显然也不是第3段所述的文件结束或读错误:

[…]如果遇到文件结尾且没有字符读入数组,则数组内容保持不变,并返回空指针。 如果在操作期间发生读取错误,则数组内容是不确定的,并返回空指针。

至于n == 0的最终情况,这看起来就像是未定义的行为。 C99标准草案第4.节。 符合性2段说( 强调我的 ):

如果违反了约束之外出现的”shall”或”shall not”要求,则行为未定义。 未定义的行为在本国际标准中以“未定义的行为” 或“省略行为的任何明确定义一词另有说明 。 这三者之间的重点没有区别; 他们都描述”未定义的行为”。

C11中的措辞相同。 最多只读取-1个字符是不可能的,它既不是文件的结尾也不是读取错误。 所以我们在这种情况下没有明确的行为定义。 看起来像一个缺陷,但我找不到任何有关此问题的缺陷报告。

tl; dr:那个版本的glibc有一个n = 1的错误,规范(可以说)是n <1的模糊性; 但我认为较新的glibc是最明智的选择。

所以,c99规格基本相同。

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

test_fgets(s,0)的行为是未定义的。 它没有成功(你最多不能读取-1个字符),但它没有达到两个’return null’标准中的任何一个(EOF&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 (在我看来)。

C标准(C11 n1570草案fgets()这种方式指定fgets() (一些强调我的):

7.21.7.2 fgetsfunction

概要

  #include  char *fgets(char * restrict s, int n, FILE * restrict stream); 

描述

fgets函数最多只读取一个小于n指定的字符数,该值由 stream指向的stream指向s指向的数组。 在换行符(保留)或文件结束后不会读取其他字符。 在读入数组的最后一个字符后立即写入空字符。

返回

如果成功, fgets函数返回s 。 如果遇到文件结尾且没有字符读入数组,则数组的内容保持不变,并返回空指针。 如果在操作期间发生读取错误,则数组内容是不确定的,并返回空指针。

该短语最多读取的次数少于n指定的字符数不够精确。 负数不能表示*字符数**,但0表示没有字符最多读取-1个字符似乎不可能,因此未指定n <= 0情况。

对于n = 1fgets被指定为最多读取0个字符,除非流无效或处于错误状态,否则它应该成功。 在读入数组的最后一个字符是不明确的,因为没有字符被读入数组时,短语A null字符被立即写入 ,但将此特殊解释为s[0] = '\0';是有意义s[0] = '\0';gets_s的规范提供相同的读数,具有相同的不精确性。

snprintf的规范更精确,明确指定n = 0的情况,附加有用的语义。 不幸的是,这样的语义不能用于fgets

7.21.6.5 snprintf函数

概要

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

描述

snprintf函数等效于fprintf ,除了输出被写入数组(由参数s指定)而不是流。 如果n为零,则不写入任何内容, s可以是空指针。 否则,将丢弃超出n-1 st的输出字符而不是写入数组,并在实际写入数组的字符末尾写入空字符。 如果在重叠的对象之间进行复制,则行为未定义。

get_s()的规范还阐明了n = 0的情况并使其成为运行时约束违规:

K.3.5.4.1 gets_s函数

概要

 #define __STDC_WANT_LIB_EXT1__ 1 #include  char *gets_s(char *s, rsize_t n); 

运行约束

s不应是空指针。 n既不应等于零也不应大于RSIZE_MAX 。 从stdin读取n-1字符时,应出现新行字符,文件结束或读取错误。

如果存在运行时约束违规,则将s[0]设置为空字符,并从stdin读取和丢弃字符,直到读取换行符,或发生文件结束或读取错误。

描述

gets_s函数最多只读取一个小于n指定的字符数,由stdin指向的流指向s指向的数组。 在换行符(被丢弃)之后或文件结束之后,不会读取其他字符。 丢弃的换行符不计入读取的字符数。 在读入数组的最后一个字符后立即写入空字符。

如果遇到文件结束且没有字符读入数组,或者在操作期间发生读取错误,则s[0]设置为空字符, s的其他元素采用未指定的值。

推荐做法

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

返回

如果成功, gets_s函数返回s 。 如果存在运行时约束违规,或者遇到文件结束且没有字符读入数组,或者在操作期间发生读取错误,则返回空指针。

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

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

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