C - scanf () против get () против fgets () - PullRequest
32 голосов
/ 21 июля 2010

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

После того, как я закончил, я заметил некоторые очень специфические "ошибки", которые я могуне отвечаю, в основном из-за моих ограниченных знаний о том, как работают функции scanf(), gets() и fgets().(Хотя я действительно читал много литературы.)

Итак, не написав слишком много текста, вот код программы:

#include <stdio.h>

#define MAX 100

int CharToInt(const char *);

int main()
{
    char str[MAX];

    printf(" Enter some numbers (no spaces): ");
    gets(str);
//  fgets(str, sizeof(str), stdin);
//  scanf("%s", str);

    printf(" Entered number is: %d\n", CharToInt(str));

    return 0;
}

int CharToInt(const char *s)
{
    int i, result, temp;

    result = 0;
    i = 0;

    while(*(s+i) != '\0')
    {
        temp = *(s+i) & 15;
        result = (temp + result) * 10;
        i++;
    }

    return result / 10;
}

Итак, вот проблема, с которой я столкнулся.Во-первых, при использовании функции gets() программа работает отлично.

Во-вторых, при использовании fgets() результат немного неверный, потому что, очевидно, функция fgets() читает символ новой строки (значение ASCII 10), последний из которых винтвверх результат.

В-третьих, при использовании функции scanf() результат совершенно неверный, поскольку первый символ, очевидно, имеет значение -52 ASCII.Для этого у меня нет объяснения.

Теперь я знаю, что gets() не рекомендуется использовать, поэтому я хотел бы знать, могу ли я использовать fgets() здесь, чтобы он не читал (или игнорировал)символ новой строки.Кроме того, в чем же заключается функция scanf() в этой программе?

Ответы [ 7 ]

25 голосов
/ 21 июля 2010
  • Никогда использовать gets. Он не предлагает защиты от уязвимости переполнения буфера (то есть вы не можете сказать ему, насколько велик буфер, который вы передаете ему, поэтому он не может помешать пользователю ввести строку, превышающую размер буфера и переполнение памяти). 1007 *

  • Избегайте использования scanf. Если не использовать осторожно, он может иметь те же проблемы переполнения буфера, что и gets. Даже игнорируя это, у есть другие проблемы, которые затрудняют правильное использование .

  • Как правило, вместо этого следует использовать fgets, хотя иногда это неудобно (вам нужно убрать символ новой строки, вы должны заранее определить размер буфера, а затем выяснить, что делать со строками, которые слишком долго - оставляете ли вы ту часть, которую прочитали, и отбрасываете лишнее , отбрасываете все это, динамически увеличиваете буфер и попробуйте снова и т. д.). Доступно несколько нестандартных функций, которые выполняют это динамическое распределение для вас (например, getline в системах POSIX, функция Чака Фальконера ggets). Обратите внимание, что ggets имеет gets -подобную семантику в том смысле, что он лишает вас новой строки.

19 голосов
/ 21 июля 2010

Да, вы хотите избежать gets.fgets всегда будет читать новую строку, если буфер был достаточно большим, чтобы его вместить (что позволяет узнать, когда буфер был слишком мал и есть больше строк, ожидающих чтения).Если вы хотите что-то вроде fgets, которое не будет читать новую строку (потеряв это указание на слишком маленький буфер), вы можете использовать fscanf с преобразованием набора сканирования, например: "%N[^\n]", где 'N'заменяется размером буфера - 1.

Один простой (если странный) способ удалить завершающую новую строку из буфера после чтения с помощью fgets: strtok(buffer, "\n"); Это не так strtok предназначен для использования, но я использовал его чаще, чем предполагалось (чего я обычно избегаю).

10 голосов
/ 08 июля 2015

Есть множество проблем с этим кодом. Мы исправим плохо названные переменные и функции и исследуем проблемы:

  • Во-первых, CharToInt() следует переименовать на правильный StringToInt(), поскольку он работает с строкой , а не одним символом.

  • Функция CharToInt() [sic.] Небезопасна. Он не проверяет, случайно ли пользователь передал нулевой указатель.

  • Не проверяет ввод, или, что более правильно, пропускает неверный ввод. Если пользователь вводит не-цифру, результат будет содержать фиктивное значение. т.е. если вы введете N, то код *(s+i) & 15 будет выдавать 14!?

  • Далее, невзрачный temp в CharToInt() [sic.] Должен называться digit, поскольку это действительно так.

  • Кроме того, kludge return result / 10; - это просто плохой хак для обхода ошибочной реализации.

  • Аналогично MAX имеет неправильное название, так как может показаться, что он конфликтует со стандартным использованием. т.е. #define MAX(X,y) ((x)>(y))?(x):(y)

  • Подробный *(s+i) не так удобен для чтения, как просто *s. Нет необходимости использовать и загромождать код еще одним временным индексом i.

получает ()

Это плохо, потому что может переполнить буфер входной строки. Например, если размер буфера равен 2 и вы вводите 16 символов, вы переполнитесь str.

зсапЕ ()

Это в равной степени плохо, поскольку может переполнить буфер входной строки.

Вы упоминаете " при использовании функции scanf (), результат совершенно неверный, потому что первый символ, очевидно, имеет значение -52 ASCII. "

Это связано с неправильным использованием scanf (). Я не смог продублировать эту ошибку.

fgets ()

Это безопасно, потому что вы можете гарантировать, что никогда не переполните буфер входной строки, передавая размер буфера (который включает в себя пространство для NULL.)

GetLine ()

Несколько человек предложили стандарт C POSIX getline() в качестве замены. К сожалению, это не практичное портативное решение, так как Microsoft не реализует версию C; только стандартная функция шаблона строки C ++ , так как этот вопрос SO # 27755191 отвечает. Microsoft C ++ getline() была доступна, по крайней мере, еще как Visual Studio 6 , но, поскольку OP строго спрашивает о C, а не C ++, это не вариант.

Разное.

Наконец, эта реализация содержит ошибки, поскольку она не обнаруживает целочисленное переполнение. Если пользователь вводит слишком большое число, число может стать отрицательным! то есть 9876543210 станет -18815698 ?! Давайте это тоже исправим.

Это тривиально исправить для unsigned int. Если предыдущий частичный номер меньше текущего частичного номера, то мы переполнились и возвращаем предыдущий частичный номер.

Для signed int это немного больше работы. В сборке мы могли бы проверить флаг переноса, но в C нет стандартного встроенного способа обнаружения переполнения с помощью встроенной математической функции. К счастью, поскольку мы умножаем на постоянную, * 10, мы можем легко обнаружить это, если используем эквивалентное уравнение:

n = x*10 = x*8 + x*2

Если x * 8 переполнен, то логически x * 10 также будет. Для 32-битного int переполнение произойдет, когда x * 8 = 0x100000000, поэтому все, что нам нужно сделать, это обнаружить, когда x> = 0x20000000. Поскольку мы не хотим предполагать, сколько бит имеет int, нам нужно только проверить, установлены ли старшие 3 мсб (наиболее значимые биты).

Кроме того, необходим второй тест на переполнение. Если msb устанавливается (бит знака) после объединения цифр, то мы также знаем, что число переполнено.

Код

Вот исправленная безопасная версия вместе с кодом, с которым вы можете играть, чтобы обнаружить переполнение в небезопасных версиях. Я также включил версии signed и unsigned через #define SIGNED 1

#include <stdio.h>
#include <ctype.h> // isdigit()

// 1 fgets
// 2 gets
// 3 scanf
#define INPUT 1

#define SIGNED 1

// re-implementation of atoi()
// Test Case: 2147483647 -- valid    32-bit
// Test Case: 2147483648 -- overflow 32-bit
int StringToInt( const char * s )
{
    int result = 0, prev, msb = (sizeof(int)*8)-1, overflow;

    if( !s )
        return result;

    while( *s )
    {
        if( isdigit( *s ) ) // Alt.: if ((*s >= '0') && (*s <= '9'))
        {
            prev     = result;
            overflow = result >> (msb-2); // test if top 3 MSBs will overflow on x*8
            result  *= 10;
            result  += *s++ & 0xF;// OPTIMIZATION: *s - '0'

            if( (result < prev) || overflow ) // check if would overflow
                return prev;
        }
        else
            break; // you decide SKIP or BREAK on invalid digits
    }

    return result;
}

// Test case: 4294967295 -- valid    32-bit
// Test case: 4294967296 -- overflow 32-bit
unsigned int StringToUnsignedInt( const char * s )
{
    unsigned int result = 0, prev;

    if( !s )
        return result;

    while( *s )
    {
        if( isdigit( *s ) ) // Alt.: if (*s >= '0' && *s <= '9')
        {
            prev    = result;
            result *= 10;
            result += *s++ & 0xF; // OPTIMIZATION: += (*s - '0')

            if( result < prev ) // check if would overflow
                return prev;
        }
        else
            break; // you decide SKIP or BREAK on invalid digits
    }

    return result;
}

int main()
{
    int  detect_buffer_overrun = 0;

    #define   BUFFER_SIZE 2    // set to small size to easily test overflow
    char str[ BUFFER_SIZE+1 ]; // C idiom is to reserve space for the NULL terminator

    printf(" Enter some numbers (no spaces): ");

#if   INPUT == 1
    fgets(str, sizeof(str), stdin);
#elif INPUT == 2
    gets(str); // can overflows
#elif INPUT == 3
    scanf("%s", str); // can also overflow
#endif

#if SIGNED
    printf(" Entered number is: %d\n", StringToInt(str));
#else
    printf(" Entered number is: %u\n", StringToUnsignedInt(str) );
#endif
    if( detect_buffer_overrun )
        printf( "Input buffer overflow!\n" );

    return 0;
}
4 голосов
/ 21 июля 2010

Вы правы, что никогда не должны использовать gets.Если вы хотите использовать fgets, вы можете просто перезаписать новую строку.

char *result = fgets(str, sizeof(str), stdin);
char len = strlen(str);
if(result != NULL && str[len - 1] == '\n')
{
  str[len - 1] = '\0';
}
else
{
  // handle error
}

Это предполагает, что встраиваемых NULL нет.Другой вариант - POSIX getline:

char *line = NULL;
size_t len = 0;
ssize_t count = getline(&line, &len, stdin);
if(count >= 1 && line[count - 1] == '\n')
{
  line[count - 1] = '\0';
}
else
{
  // Handle error
}

Преимущество getline в том, что он выполняет распределение и перераспределение для вас, обрабатывает возможные встроенные значения NULL и возвращает счетВам не нужно тратить время на strlen.Обратите внимание, что вы не можете использовать массив с getline.Указатель должен быть NULL или свободен.

Я не уверен, что у вас проблема с scanf.

3 голосов
/ 21 июля 2010

никогда не используйте get (), это может привести к непредсказуемым переполнениям. Если ваш строковый массив имеет размер 1000 и я ввожу 1001 символ, я могу переполнить буфер вашей программы.

1 голос
/ 21 июля 2010

Попробуйте использовать fgets () с этой измененной версией вашего CharToInt ():

int CharToInt(const char *s)
{
    int i, result, temp;

    result = 0;
    i = 0;

    while(*(s+i) != '\0')
    {
        if (isdigit(*(s+i)))
        {
            temp = *(s+i) & 15;
            result = (temp + result) * 10;
        }
        i++;
    }

    return result / 10;
}

По существу, проверяет вводимые цифры и игнорирует все остальное. Это очень грубо, поэтому измените его и посолите по вкусу.

0 голосов
/ 02 декабря 2014

Так что я не большой программист, но позвольте мне попытаться ответить на ваш вопрос о scanf();. Я думаю, что Scanf довольно хорошо и использовать его в основном для всего, без каких-либо проблем. Но вы приняли не совсем правильную структуру. Должно быть:

char str[MAX];
printf("Enter some text: ");
scanf("%s", &str);
fflush(stdin);

«&» перед переменной имеет важное значение. Он сообщает программе, где (в какой переменной) сохранить отсканированное значение. fflush(stdin); очищает буфер от стандартного ввода (клавиатуры), поэтому вероятность переполнения буфера меньше.

И разница между gets / scanf и fgets заключается в том, что gets(); и scanf(); сканируют только до первого пробела ' ', пока fgets(); сканирует весь ввод (но после этого обязательно очистите буфер, чтобы потом не было переполнения)

...