Можно ли правильно читать нулевые символы, используя fgets или gets_s?

Предположим, я хочу читать с stdin, и пусть пользователь вводит строки, которые содержат нулевые символы. Возможно ли это с помощью функций ввода строки, таких как fgets или же gets_s? Или я должен использовать, например, fgetc или же fread?

Кто-то здесь хотел сделать это.

4 ответа

Решение

Можно ли правильно читать нулевые символы, используя fgets или gets_s?

Как показывают некоторые другие ответы, ответ, по-видимому, "Да, только едва". Точно так же можно забивать гвозди с помощью отвертки. Точно так же можно написать (что составляет) код BASIC или FORTRAN на C.

Но ни одна из этих вещей не является хорошей идеей. Используйте правильный инструмент для работы. Если вы хотите забить гвозди, используйте молоток. Если вы хотите написать BASIC или FORTRAN, используйте интерпретатор BASIC или компилятор FORTRAN. И если вы хотите прочитать двоичные данные, которые могут содержать нулевые символы, используйте fread (или, может быть getc). Не использовать fgetsпотому что его интерфейс никогда не был предназначен для этой задачи.

За fgets, да. fgets указано вести себя как будто при повторном fgetc и хранение полученных символов в массиве. Никаких специальных условий для нулевого символа не предусмотрено, за исключением того, что в дополнение к прочитанным символам в конце хранится нулевой символ (после последнего символа).

Однако, чтобы успешно отличить встроенные нулевые символы от окончания, требуется некоторая работа.

Во-первых, предварительно заполните буфер '\n' (например, используя memset). Теперь, когда fgets возвращается, ищите первый '\n' в буфере (например, используя memchr).

  • Если нет '\n', fgets остановлен из-за заполнения выходного буфера, и все, кроме последнего байта (нулевого терминатора), являются данными, которые были прочитаны из файла.

  • Если первый '\n' немедленно сопровождается '\0' (нулевое окончание), fgets остановлен из-за достижения новой строки, и все, что было до этой новой строки, было прочитано из файла.

  • Если первый '\n' не сопровождается '\0' (либо в конце буфера, либо после другого '\n') затем fgets остановлен из-за EOF или ошибки, и все до байта непосредственно перед '\n' (который обязательно '\0') но не включая его, был прочитан из файла.

За gets_sЯ понятия не имею, и я настоятельно рекомендую не использовать его. Единственная широко реализованная версия функций "*_s" Приложения K, Microsoft, даже не соответствует спецификациям, которые они выдвинули в приложение стандарта C, и, как сообщается, имеет проблемы, которые могут сделать этот подход неэффективным.

Можно ли правильно читать нулевые символы, используя fgets или же gets_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(), хотя меня это не полностью устраивает.

Есть способ надежно обнаружить наличие \0 символы, прочитанные fgets(3) но это очень неэффективно. Чтобы надежно определить, что из входного потока прочитан нулевой символ, вы должны сначала заполнить буфер ненулевыми символами. Причина этого в том, что fgets() разграничить его ввод, поместив \0 в конце ввода и (он должен) больше ничего не пишет после этого символа.

Ну, после заполнения входного буфера, скажем, \001 chars, call fgets() в вашем буфере, и начнем поиск с конца буфера назад для \0 символ: это конец входного буфера. Нет необходимости проверять персонажа раньше (единственный случай, когда \n если последний символ \0 и строка ввода была длиннее, чем пространство в буфере для полной строки с нулевым окончанием или фиктивной реализации fgets(3) (есть некоторые). С самого начала вы можете иметь как можно больше \0S, как это может быть, но не волнуйтесь, они из входного потока.

Как видите, это довольно неэффективно.

#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 */
Другие вопросы по тегам