fgets()是否在短缓冲区兼容的情况下返回NULL?

时间:2014-04-30 12:51:52

标签: c language-lawyer fgets

在单元测试中,包含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()返回NULL feof()ferror()都不是1

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

问题:

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

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

以下是C11标准的摘要,其中一些重点是我的:

  

7.21.7.2 fgets功能

     

fgets 函数最多读取的数字少于 n [...]

指定的字符数      

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

相关帖子
How to use feof and ferror for fgets (minishell in C)
Trouble creating a shell in C (Seg-Fault and ferror)
fputs(), fgets(), ferror() questions and C++ equivalents
Return value of fgets()


[编辑]对答案的评论

@Shafik Yaghmour很好地介绍了整体问题:因为C规范没有提到当它不读取任何数据时要做什么,也没有将任何数据写入{{ 1}}当(s)时,它是未定义的行为。因此任何合理的响应都应该是可接受的,例如return n <= 0,设置无标志,单独保留缓冲区。

至于NULL时会发生什么,@ Oliver Matthews的回答和@Matt McNabb的评论表明,考虑到n==1的缓冲区,C规范缺乏明确性。 C规范似乎以支持n == 1的缓冲区应该返回带有n == 1的缓冲区指针,但是不够明确。

3 个答案:

答案 0 :(得分:3)

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

  

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

     

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

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

  

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

至于最后一种情况,n == 0看起来像是未定义的行为。 C99标准部分草案4. 一致性 2 表示(强调我的):

  

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

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

答案 1 :(得分:2)

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

所以,c99规范基本相同。

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

test_fgets(s,0)的行为确实是未定义的。它没有成功(你不能读取最多-1个字符),但它没有达到两个'return null'标准中的任何一个(EOF&amp; 0读取;读取错误)。

但是,GCC的行为可以说是正确的(将指针返回到未更改的s也可以) - feof未设置,因为它没有达到eof;未设置ferror,因为没有读取错误。

我怀疑gcc中的逻辑(没有得到源代码)在顶部附近有一个'if n&lt; = 0 return null'。

[编辑:]

经过反思,我实际上认为glibc对n=0的行为是最正确的反应:

  • 没有自我阅读,所以feof()==0
  • 没有读取,因此不会发生读取错误,因此ferror=0

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

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

答案 2 :(得分:1)

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

  

7.21.7.2 fgets功能

     

<强>概要

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

<强>描述

     

fgets函数从n指向的流中读取最多比stream 指定的字符数少一个s。在换行符(保留)或文件结束后不会读取其他字符。在读入数组的最后一个字符后立即写入空字符。

     

<强>返回

     

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

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

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

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

  

7.21.6.5 snprintf功能

     

概要

#include <stdio.h>
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 <stdio.h>
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设置为空字符,以及{的其他元素{1}}取未指定的值。

     

推荐做法

     

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

     

<强>返回

     

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

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

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

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