由于标准C委员会没有为gets()标准化一个简单的替代方法,应该使用什么替代方法?

20
gets函数在C99中被首次弃用,并在C11最终被移除。但是,在C库中没有直接替代它的函数。

fgets()不是一种完全替代的方法,因为它不会删除最后的'\n',而这个字符可能不存在于文件末尾。许多程序员也会犯错。

有一个简单的一行代码可以删除换行符:buf[strcspn(buf,"\n")]= '\ 0';,但它并不简单,通常需要解释。这种方法也可能效率低下。

这种情况是适得其反的。许多初学者仍然使用gets(),因为他们的老师过时或教程过时。

微软提出了gets_s()和许多相关函数,但它不会静默截断过长的行,这种限制违约的行为不是很简单。

BSD和GNU libc都有getline,在POSIX中进行了标准化,该函数通过realloc分配或重新分配缓冲区......

如何向初学者介绍这个混乱局面是最好的方式?


1
POSIX的getlinefgets更加复杂。使用%[scanf是另一个选项,但它也有自己的缺陷。对我来说,fgets似乎并不太糟糕,它的优点是能够告诉我们行是否超出了缓冲区。 - M.M
1
@M.M 使用puts,人们会问:“那么gets有什么问题呢?”请记住,这个问题的性质是教学而不是技术性的。 - user824425
2
这将是讲解gets究竟有多糟糕的绝佳机会。初学者的C语言教育必须包括缓冲区溢出的讨论,并强调不要这样做的重要性。 - M.M
1
@M.M:scanf绝对不是gets()的一个好替代品。它的大小限制参数偏差一,必须在格式字符串中明确指定,这样非常不优雅!scanf_s稍微好一些,但在BSD和Linux上不受支持,就像gets_s一样... - chqrlie
2
好的,我相信委员会成员们有时会阅读stackoverflow,如果他们没有阅读,那么也许会有证据说明为什么他们没有提供直接替换或推荐替代方案。我也想知道如何最好地教初学者这个问题。 - chqrlie
显示剩余6条评论
3个回答

6
这个问题的性质决定了会有一些猜测和意见。但是我们可以从C99的解释和C11标准中找到一些信息。
gets()被弃用时,C99 rationale给出了以下原因:
“由于gets不检查缓冲区溢出,当其输入不受程序员控制时使用它通常是不安全的。这使得一些人质疑它是否应该出现在标准中。委员会决定,在程序员具有足够控制权的特殊情况下,gets是有用和方便的,并且作为长期存在的实践,它需要一个标准规范。然而,一般来说,首选函数是fgets(参见§7.19.7.2)。”
我认为gets_s()也不能被视为替代品。因为gets_s()是一个可选接口。C11实际上推荐使用fgets()而不是gets_s()

§K.3.5.4.1,C11草案

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

因此,在ISO C中,我们只能使用fgets()来替换gets()fgets()等效于gets(),除非有缓冲区空间,否则会读取换行符。 那么,引入一个仅具有与长期广泛使用的fgets()略有改进的新接口值得吗?在我看来,不值得。

此外,许多真实世界的应用程序并不限于仅使用ISO C。因此,可以使用扩展和POSIX getline()等作为替代方案。

如果必须在ISO C中编写解决方案,那么编写一个包装器来包装fgets()就很容易了,例如my_fgets(),它会删除换行符(如果存在)。当然,教授fgets()给新手需要解释潜在的换行符问题。但我认为,这并不难理解,打算学习C语言的人应该能够很快掌握它。甚至可以将此视为初学者的好练习。因此,基于上述原因,我认为没有必要在ISO C中添加一个新函数来真正替代gets()。

4

这个问题需要引用委员会记录或其他材料才能得到具有说服力的答案。但总的原则是,委员会(WG14)通常避免发明新接口,而倾向于记录和规范现有的实践(例如snprintf, long long, inttypes.h类型等),有时也会采用其他标准/接口定义中的内容(例如来自IEEE浮点数的复杂数学、C++的原子模型等)。由于gets没有替代方案可供采用,可能是因为fgets通常被认为是更好的选择(当文件结尾没有换行符时,它不会丢失数据)。如果您真的需要直接替换,可以使用以下代码:

char buf[100];
scanf("%99[^\n]%*1[\n]", buf);

当然,如果缓冲区大小是可变的,使用起来会很笨拙。

6
委员会(WG14)通常避免发明新的接口。 你在开玩笑吗?他们发明了许多价值可疑的多字符接口。 - chqrlie
3
这段代码无法处理空行(即流中的裸'\n'):如果没有匹配到任何字符,%[匹配失败,因此它不会继续执行下一个指示符。 - M.M
2
@M.M:在这种情况下,您还需要一个单独的操作来对(未写入的)buf进行空终止。 - R.. GitHub STOP HELPING ICE
2
值得指出的是,wchar_t 可以是 16 位,因为 Unicode 最初被设计为 16 位。 - user253751
2
@chqrlie:那实际上是行不通的。mbrtowc只能产生一个wchar_t,而wcrtomb只能处理一个。uchar16_t函数解除了这个API限制,因此UTF-16可以得到支持,但Windows存在缺陷,非BMP码点在C mb/wc API中根本无法工作。(当然,他们希望你忽略标准API,改用WinAPI函数...) - R.. GitHub STOP HELPING ICE
显示剩余10条评论

2

在我看来,任何替代方案都需要通过sizechar *目标传递,这需要根据具体情况进行重大的代码更改。一种通用的解决方案是不可行的,因为size往往在代码到达gets()时丢失/未传递。考虑到我们已经有了12年的警告(从C99到C11),委员会认为这个问题在2011年之前就会消失。

哈!

标准C委员会应该制定一个替代方案,也要传递目标大小,如下所示。(这可能存在名称冲突问题)

char *gets_replacement(char *s, size_t size);

我尝试使用基于fgets()的替代方法,利用可变长度数组(在C11中是可选的)

char *my_gets(char *dest, size_t size) {
  // +2 one for \n and 1 to detect overrun
  char buf[size + 2];

  if (fgets(buf, sizeof buf, stdin) == NULL) {
    // improve error handling - see below comment
    if (size > 0) {
      *buf = '\0';
    }
    return NULL;
  }
  size_t len = strlen(buf);
  if (len > 0 && buf[len - 1] == '\n') {
    buf[--len] = '\0';
  }

  // If input would have overrun the original gets()
  if (len >= size) {
    // or call error handler
    if (size > 0) {
      *buf = '\0';
    }
    return NULL;  
  }
  return memcpy(dest, buf, len + 1);
}

2
考虑到我们有12年的警告时间(从C99到C11),委员会可能认为这个问题在2011年之前就已经消失了。但是根据我们在stackoverflow上的经验,这个问题非常棘手。初学者仍然使用gets函数。 - chqrlie
1
您提出的实现与 fgets 存在相同的缺陷:在读取错误或输入过长时,buf 是不确定的。我认为,在这种情况下,如果 size > 0,最好将 *buf = '\0'; 设置为默认值。fgets 在 EOF 时保持缓冲区不变,但这种指定行为比有用更容易出错。 - chqrlie
@chqrlie 同意。 "哈!"是为了表明我不同意这样的推理,虽然在1999年很有希望,但到2011年并没有实现。 - chux - Reinstate Monica
@chqrlie,我同意你的想法。一个过于追求细节的解决方案会更深入地挖掘ferror()feof(),以便在fgets()返回NULL时进行处理。使用fgets()时,在feof()上缓冲区保持不变,但在ferror()上是不确定的。我怀疑gets()也是这样工作的。因此,在ferror()上使用*buf = '\0'可能是有道理的。 - chux - Reinstate Monica
@chqrlie 在第二种情况下,我使用了不可见的交换(代码行)例程。现在已经修复。 - chux - Reinstate Monica

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接