使用fgets或gets_s函数能够正确读取空字符吗?

5
假设我想从stdin读取,并让用户输入包含空字符的字符串。像fgetsgets_s这样的字符串输入函数是否可以实现?或者我必须使用例如fgetcfread
有人在这里想要做到这一点。

它是否也包含\n - Eugene Sh.
链接问题的帖子中,不清楚作者是否“想要”这样做。此外,不清楚他对gets_s的实现是否随机读取了行末字符,但如果确实如此,那么它几乎使得任何答案(例如@R.的答案)都无效,并且回答是“不,这是不可能的”。 - Steve Summit
这可能勉强可行,但显然是个坏主意。 - Steve Summit
4个回答

7
对于 fgets,是的。fgets 的行为被规定为重复调用fgetc 并将所得到的字符存储到数组中。对于空字符,没有特别的规定,除了在读取完所有字符后会在末尾(最后一个字符之后)存储一个空字符。
然而,要成功地区分嵌入的空字符和终止符需要一些工作。
首先,使用 memset 填充缓冲区以准备好'\n'。现在,当fgets 返回时,在缓冲区中查找第一个'\n'(例如使用memchr)。
  • 如果没有'\n',则fgets 停止,因为输出缓冲区已满,并且除了最后一个字节(空终止符)之外的所有内容均为从文件中读取的数据。

  • 如果第一个'\n' 紧随其后的是 '\0'(空终止符),则fgets 停止,因为达到了换行符,且一直到该换行符前的所有内容都是从文件中读取的。

  • 如果第一个'\n'没有随后跟随'\0'(在缓冲区末尾或后面紧随另一个'\n'),则fgets 停止,因为已经到达EOF或发生错误,此时直到'\n'之前的字符(这是必须是一个'\0')但不包括它,都是从文件中读取的。

对于gets_s,我不知道,并且强烈建议不要使用。 Annex K "*_s" 函数的唯一被广泛实现的版本即 Microsoft 的版本,甚至不符合C标准附录中所规定的规范,据报道还存在问题可能使这种方法无法正常工作。

4
使用fgetsgets_s读取空字符是否正确?事实上并不完全正确。 fgets()未规定在添加'\0'后是否保留缓冲区的其余部分,因此预先加载缓冲区进行后续分析可能无法正常工作。
在“读错误”情况下,缓冲区被指定为“数组内容是不确定的”,但通过检查返回值可以消除这种情况的进一步关注。
如果没有这个问题,那么可以像@R..建议的那样进行各种测试。
  char buf[80];
  int length = 0;
  memset(buf, sizeof buf, '\n');
  // Check return value before reading `buf`.
  if (fgets(buf, sizeof buf, stdin)) {
    // The buffer should end with a \0 and 0 to 78 \n
    // Starting at the end, look for the first non-\n
    int i = sizeof buf - 1;
    while (i > 0) {
      if (buf[i] != '\n') {
        if (buf[i] == '\0') {
          // found appended null
          length = i;
        } else {
          length = -1;  // indeterminent length
        }
        break;
      }
      i--;
    }
    if (i == 0) {
      // entire buffer was \n
      length = -1;  // indeterminent length
    }
  }

fgets() 在读取用户输入时可能会遇到包含空字符的情况而无法完全胜任。这在 C 中仍然存在问题。

我试图编写这个 fgets() 的替代方案,虽然我对此并不十分满意。


“未指定保留缓冲区其余部分”这一说法是不正确的,因为并不需要明确规定;相反,只需缺乏任何修改应用程序所属存储器的规定即可保证它不会这样做。 - R.. GitHub STOP HELPING ICE
@R.. fgets()在读取错误时指定缓冲区状态为“数组内容不确定”。但是,在其他情况下,它对缓冲区状态的后续部分保持沉默。随着4.2和“未定义行为是...通过没有明确定义行为来省略。”以及fgets()可以访问整个缓冲区,我断言不依赖未写部分的稳定性是合理的。你的说法也同样值得注意。 - chux - Reinstate Monica
有趣的是,最新(以及可能更早的)C规范,在_strcpy_s函数下,确实讨论了缓冲区的末尾:“当strcpy_s返回时,由strcpy_s在指向s1max字符的s1数组中写入的终止空字符(如果有)之后的所有元素都具有未指定的值。” - chux - Reinstate Monica
1
是的,Annex K 只是草率地添加进来的,几乎所有人都认为它应该被移除。当时唯一声称实现 Annex K 的(MSVC)甚至不符合已经添加到 C 中的规范,因此符合规范的实现将与唯一先前存在的实现存在微妙的不兼容性。这就像有人只是试图破坏标准化过程而不是做出有用的贡献。 - R.. GitHub STOP HELPING ICE
@R 对于 K 实现的一般评估表示认同。然而,附录整体上试图解决库中更高级部分未讨论的边缘情况。我发现这个关于缓冲区剩余部分的规范很有趣,因为它确实讨论了我们在这里使用的类似问题 - 即使我们并不完全同意 - 因此可能提供一些见解。 - chux - Reinstate Monica

2

有一种可靠的方法可以检测到fgets(3)读取的\0字符的存在,但效率非常低。为了可靠地检测输入流中是否有空字符,您必须先用非空字符填充缓冲区。原因是fgets()通过在输入的末尾放置一个\0字符来界定其输入,并且(应该)不会在该字符之后写入任何其他内容。

好的,填充输入缓冲区后,调用fgets()函数读取缓冲区中的字符,然后从缓冲区的末尾向后搜索\0字符:这就是输入缓冲区的结尾。不需要检查前面的字符(唯一的情况是最后一个字符是\0且输入行比缓冲区的空间长,无法形成完整的以空字符结尾的字符串,或者是fgets(3)的错误实现(有些情况下会出现)。从开头开始,可以有任意多个\0,但不用担心,它们来自输入流。

正如您所看到的,这种方法效率相当低下。

#define NON_ZERO         1
#define BOGUS_FGETS      -2 /* -1 is used by EOF */

/**
 * variant of fgets that returns the number of characters actually read */
ssize_t variant_of_fgets(const char *buffer, const size_t sz, FILE *in)
{
    /* set buffer to non zero value */
    memset(buffer, NON_ZERO, sz);

    /* do actual fgets */
    if (!fgets(buffer, sizeof buffer, stdin)) {
        /* EOF */
        return EOF;
    }
    char *p = buffer + sizeof buffer; 
    while (--p >= buffer)
        if (!*p) 
            break; /* if char is a \0 we're out */
    /* ASSERT: (p < buffer)[not-found] || (p >= buffer)[found] */
    if (p <= buffer) { 
        /* Why do we check for p <= buffer ?
         * p must be > buffer, as if p == buffer
         * the implementation must be also bogus, because
         * the returned string should be an empty string "".
         * this can happen only with a bogus implementation
         * or an absurd buffer of length one (with only place for
         * the \0 char).  Else, it must be a read character
         * (it can be a \0, but then it must have another \0 
         * behind, and p must be greater than this) */
        return BOGUS_FGETS;
    }
    /* ASSERT: p > buffer && p < buffer + sz  [found a \0] 
     * p points to the position of the last \0 in the buffer */ 

    return p - buffer;  /* this is the string length */
} /* variant_of_fgets */ 

示例

以下示例代码将说明该事项,首先是执行示例:

$ pru
===============================================
<OFFSET> : pru.c:24:main: buffer initial contents
00000000 : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 : ................
00000010 : e0 dd cf eb 02 56 00 00 e0 d7 cf eb 02 56 00 00 : .....V.......V..
00000020
<OFFSET> : pru.c:30:main: buffer after memset
00000000 : fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa : ................
00000010 : fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa : ................
00000020
^@^@^@^@^D^D
<OFFSET> : pru.c:41:main: buffer after fgets(returned size should be 4)
00000000 : 00 00 00 00 00 fa fa fa fa fa fa fa fa fa fa fa : ................
00000010 : fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa : ................
00000020
===============================================
<OFFSET> : pru.c:24:main: buffer initial contents
00000000 : 00 00 00 00 00 fa fa fa fa fa fa fa fa fa fa fa : ................
00000010 : fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa : ................
00000020
<OFFSET> : pru.c:30:main: buffer after memset
00000000 : fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa : ................
00000010 : fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa : ................
00000020
^D
<OFFSET> : pru.c:41:main: buffer after fgets(returned size should be 0)
00000000 : fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa : ................
00000010 : fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa : ................
00000020
===============================================
pru.c:45:main: END OF PROGRAM
$ _

Makefile

RM ?= rm -f

targets = pru
toclean += $(targets)

all: $(targets)
clean:
    $(RM) $(toclean)

pru_objs = pru.o fprintbuf.o
toclean += $(pru_objs)

pru: $(pru_objs)
    $(CC) -o $@ $($@_objs)

pru.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#include "fprintbuf.h"

#define F(fmt) __FILE__":%d:%s: " fmt, __LINE__, __func__

void line()
{
    puts("===============================================");
}
int main()
{
    uint8_t buffer[32];
    int eof;

    line();
    do {
        fprintbuf(stdout,
                buffer, sizeof buffer, 
                F("buffer initial contents"));

        memset(buffer, 0xfa, sizeof buffer);

        fprintbuf(stdout,
                buffer, sizeof buffer, 
                F("buffer after memset"));

        eof = !fgets(buffer, sizeof buffer, stdin);

        /* search for the last \0 */
        uint8_t *p = buffer + sizeof buffer;
        while (*--p && (p > buffer))
            continue;

        if (p <= buffer)
            printf(F("BOGUS implementation"));

        fprintbuf(stdout,
                buffer, sizeof buffer,
                F("buffer after fgets(size should be %u)"),
                p - buffer);
        line();
    } while(!eof);
}

使用辅助函数,打印缓冲区内容:

fprintbuf.h

/* $Id: fprintbuf.h,v 2.0 2005-10-04 14:54:49 luis Exp $
 * Author: Luis Colorado <Luis.Colorado@HispaLinux.ES>
 * Date: Thu Aug 18 15:47:09 CEST 2005
 *
 * Disclaimer:
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *  
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
#ifndef FPRINTBUF_H
#define FPRINTBUF_H

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

#include <stdio.h>
#include <stdint.h>

size_t fprintbuf (
    FILE               *f,      /* fichero de salida */
    const uint8_t      *b,      /* puntero al buffer */
    size_t              t,      /* tamano del buffer */
    const char         *fmt,    /* rotulo de cabecera */
                        ...);

#ifdef __cplusplus
} /* extern "C" */
#endif /* __cplusplus */

#endif /* FPRINTBUF_H */

fprintbuf.c

/* $Id: fprintbuf.c,v 2.0 2005-10-04 14:54:49 luis Exp $
 * AUTHOR: Luis Colorado <licolorado@indra.es>
 * DATE: 7.10.92.
 * DESC: muestra un buffer de datos en hexadecimal y ASCII.
 */

#include <sys/types.h>
#include <ctype.h>
#include <stdio.h>
#include <stdarg.h>
#include "fprintbuf.h"

#define     TAM_REG         16

size_t
fprintbuf(
        FILE           *f,      /* fichero de salida */
        const uint8_t  *b,      /* puntero al buffer */
        size_t          t,      /* tamano del buffer */
        const char     *fmt,    /* rotulo de cabecera */
                        ...)
{
    size_t off, i;
    uint8_t c;
    va_list lista;
    size_t escritos = 0;

    if (fmt)
            escritos += fprintf (f, "<OFFSET> : ");
    va_start (lista, fmt);
    escritos += vfprintf (f, fmt, lista);
    va_end (lista);
    escritos += fprintf (f, "\n");
    off = 0;
    while (t > 0) {
            escritos += fprintf (f, "%08lx : ", off);
            for (i = 0; i < TAM_REG; i++) {
                    if (t > 0)
                            escritos += fprintf (f, "%02x ", *b);
                    else escritos += fprintf (f, "   ");
                    off++;
                    t--;
                    b++;
            }
            escritos += fprintf (f, ": ");
            t += TAM_REG;
            b -= TAM_REG;
            off -= TAM_REG;
            for (i = 0; i < TAM_REG; i++) {
                    c = *b++;
                    if (t > 0)
                            if (isprint (c))
                                    escritos += fprintf (f, "%c", c);
                            else    escritos += fprintf (f, ".");
                    else break;
                    off++;
                    t--;
            }
            escritos += fprintf (f, "\n");
    }
    escritos += fprintf (f, "%08lx\n", off);

    return escritos;
} /* fprintbuf */

-1

使用fgets或gets_s正确读取空字符是可能的吗?

正如其他答案所示,答案显然是“是的——勉强可以”。同样地,使用螺丝刀可以钉入钉子。同样地,在C中编写(相当于)BASIC或FORTRAN代码是可能的。

但是这些事情都不是一个好主意。为工作选择正确的工具。如果您想驱动钉子,请使用锤子。如果您想编写BASIC或FORTRAN,请使用BASIC解释器或FORTRAN编译器。如果您想读取可能包含空字符的二进制数据,请使用fread(或者也许是getc)。不要使用fgets,因为其接口从未设计用于此任务。


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