Подтвердить что ты не робот

C - scanf() vs получает() vs fgets()

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

После того, как я закончил, я заметил некоторые очень странные "ошибки", на которые я не могу ответить, главным образом из-за моего ограниченного знания о том, как работают функции 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() в этой программе?

4b9b3361

Ответ 1

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

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

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

Ответ 2

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

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

Ответ 3

С этим кодом возникают многочисленные проблемы. Мы исправим сильно названные переменные и функции и исследуем проблемы:

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

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

  • Он не проверяет ввод или, вернее, пропускает недопустимый ввод. Если пользователь вводит не цифру, результат будет содержать фиктивное значение. т.е. если вы входите в 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; только стандартная С++ функция строкового шаблона, так как это вопрос SO # 27755191. Microsoft С++ getline() был доступен по крайней мере еще как Visual Studio 6, но поскольку OP строго спрашивает о C, а не С++, это isn ' t вариант.

Разное.

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

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

При a 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 (наиболее значимые биты).

Кроме того, требуется второй тест переполнения. Если 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

Вы правы, что никогда не должны использовать 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.

Ответ 5

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

Ответ 6

Попробуйте использовать 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;
}

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

Ответ 7

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

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

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

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