提问人:chux - Reinstate Monica 提问时间:4/30/2014 最后编辑:Communitychux - Reinstate Monica 更新时间:9/10/2020 访问量:3449
fgets() 是否返回带有短缓冲区的 NULL 兼容?
Is fgets() returning NULL with a short buffer compliant?
问:
在单元测试中,一个包含 的函数,当缓冲区大小 .显然,这样的缓冲区大小是愚蠢的,但测试正在探索极端情况。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 规范似乎对这种罕见的情况保持沉默。
问题:
- 是否在不设置或不合规的情况下返回?
NULL
feof()
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 、不设置标志、不理会缓冲区。s
n <= 0
NULL
至于当 时会发生什么,@Oliver Matthews 的回答和 McNabb @Matt注释表明 C 规范缺乏清晰度,考虑到 的缓冲区。C 规范似乎倾向于 的缓冲区 should 返回缓冲区指针,但不够明确。n==1
n == 1
n == 1
s[0] == '\0'
答:
TL的;DR:那个版本的 glibc 有一个 n=1 的错误,规范(可以说)对 n<1 有歧义;但我认为较新的glibc采取了最明智的选择。
所以,c99的规格基本是一样的。
行为是错误的。glibc 2.19 给出正确的输出 (, .test_fgets(s, 1)
retval!=null
s[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
评论
在较新版本的 , 中,行为是不同的,因为 ,它返回表示成功,这并不是对 fgets 函数第 2 段的不合理解读,它说(在 C99 和 C11 中都是一样的,强调我的):glibc
n == 1
s
7.19.7.2
char *fgets(char * restrict s, int n, FILE * 限制流);
fgets 函数从 stream 指向的流中读取最多比 n 指定的字符数少 1 个,进入 s 指向的数组中。没有额外的 字符在换行符(保留)之后或文件末尾之后读取。空字符在读入数组的最后一个字符之后立即写入。
不是很有用,但不违反标准中所说的任何内容,它将读取最多字符和 null 终止。因此,您看到的结果看起来像是在 的更高版本中修复的错误。它显然也不是第 3 段所述的文件结束或阅读错误:0
glibc
[...]如果遇到文件末尾,并且没有将任何字符读入数组,则数组的内容保持不变,并返回 null 指针。如果在操作过程中发生读取错误,则数组内容不确定,并返回 null 指针。
至于最后一种情况,这看起来只是未定义的行为。C99 标准草案部分一致性第 2 段说(强调我的):n == 0
4.
如果违反了出现在约束之外的“应”或“不应”要求,则该行为未定义。在本国际标准中,未定义的行为以其他方式以“未定义的行为”一词或省略任何明确的行为定义来表示。这三者在侧重点上没有区别;它们都描述了“未定义的行为”。
C11中的措辞相同。最多无法读取 -1 个字符,它既不是文件结束也不是读取错误。因此,在这种情况下,我们没有明确定义行为。看起来像一个缺陷,但我找不到任何涵盖此缺陷的缺陷报告。
评论
test_fgets(s, 0);
test_fgets(s, INT_MIN);
test_fgets(s, INT_MIN);
s
INT_MAX
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 的数组中。在换行符(保留)之后或文件末尾之后不会读取其他字符。空字符在读入数组的最后一个字符之后立即写入。fgets
stream
s
返回
如果成功,该函数将返回。如果遇到文件末尾,并且没有将任何字符读入数组,则数组的内容保持不变,并返回 null 指针。如果在操作过程中发生读取错误,则数组内容不确定,并返回 null 指针。
fgets
s
该短语最多读数比 n
指定的字符数少 1 个,这不够精确。负数不能表示多个字符*,但表示没有字符。最多读取 -1 个字符似乎是不可能的,因此标准未定义大小写,因此具有未定义的行为。0
n <= 0
对于 ,被指定为最多读取 0 个字符,除非流无效或处于错误状态,否则它应该会成功。短语 A null character is immediately write after the last character read into the array 是模棱两可的,因为没有字符被读入数组,但将这种特殊情况解释为 meaning 是有意义的。规格提供相同的读数,但精度相同。同样,该行为没有明确定义,因此它是未定义的1.n = 1
fgets
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, ...);
描述
该函数等价于 ,只不过输出被写入数组(由参数指定)而不是流中。如果
n
为零,则不写入任何内容,并且s
可能是 null 指针。否则,超出 st 的输出字符将被丢弃,而不是写入数组,并且 null 字符将写入实际写入数组的字符的末尾。如果复制发生在重叠的对象之间,则行为未定义。snprintf
fprintf
s
n-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 读取字符时,应发生换行符、文件末尾或读取错误。n
RSIZE_MAX
n-1
如果存在运行时约束冲突,则设置为 null 字符,并且从读取和丢弃字符,直到读取换行符,或者发生文件末尾或读取错误。
s[0]
stdin
描述
该函数从指向 by 的流中读取最多少于 指定的字符数 1 的字符数,进入指向 by 的数组中。在换行符(被丢弃)之后或文件末尾之后不会读取其他字符。丢弃的换行符不计入读取的字符数。空字符在读入数组的最后一个字符之后立即写入。
gets_s
n
stdin
s
如果遇到文件末尾,并且没有将任何字符读入数组,或者如果在操作过程中发生读取错误,则设置为 null 字符,而 的其他元素采用未指定的值。
s[0]
s
推荐做法
该函数允许正确编写的程序安全地处理输入行,因为输入行太长而无法存储在结果数组中。通常,这要求调用方注意结果数组中是否存在换行符。请考虑使用(以及基于换行符的任何必要处理)而不是 .
fgets
fgets
fgets
gets_s
返回
如果成功,该函数将返回。如果存在运行时约束冲突,或者遇到文件末尾并且未将任何字符读入数组,或者如果在操作过程中发生读取错误,则返回 null 指针。
gets_s
s
您正在测试的 C 库似乎在这种情况下存在一个错误,该错误已在 glibc 的更高版本中修复。返回应该意味着某种失败条件(与成功相反):文件结束或读取错误。其他情况(如无效流或未打开读取的流)或多或少被明确描述为未定义的行为。NULL
和 的情况未定义。返回是一个明智的选择,但澄清标准中对 要求的描述将是有用的,就像 的情况一样。n = 0
n < 0
NULL
fgets()
n > 0
gets_s
请注意,还有另一个规范问题:参数的类型应该是 instead of ,但这个函数最初是由 C 作者在发明之前指定的,并且在第一个 C 标准 (C89) 中保持不变。当时更改它被认为是不可接受的,因为他们试图标准化现有的用法:签名更改会在 C 库之间造成不一致,并破坏使用函数指针或未原型函数编写良好的现有代码。fgets
n
size_t
int
size_t
1C 标准在第 2 段(共 4 段)中规定。一致性 如果违反了出现在约束或运行时约束之外的“应”或“不应”要求,则该行为是未定义的。在本国际标准中,未定义的行为以其他方式以“未定义的行为”一词或省略任何明确的行为定义来表示。这三者在侧重点上没有区别;它们都描述了“未定义的行为”。
评论
NULL
s
n
n
ferror()
ferror()
NULL
fgets()
fgets()
n==1
评论
(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
*fgets(char * restrict s, int n, FILE * restrict stream);
(另见 POSIXfgets()
),大小为 an 而不是 .这意味着这可能是负面的,但我不确定如果是这样会发生什么。空字节没有空间,因此它不应该写入任何内容。返回 NULL 是合理的。POSIX可以设置,但没有提到,这是最合适的。fgets()
int
size_t
n
errno
EINVAL