Как читать/анализировать ввод в C? Часто задаваемые вопросы

У меня проблемы с моей программой на C, когда я пытаюсь читать/анализировать ввод.

Справка


Это элемент часто задаваемых вопросов.

У StackOverflow есть много вопросов, связанных с чтением ввода в C, причем ответы обычно фокусируются на конкретной проблеме этого конкретного пользователя, не рисуя всю картину.

Это попытка охватить множество распространенных ошибок всесторонне, поэтому на это конкретное семейство вопросов можно ответить просто, пометив их как дубликаты этого:

  • Почему вторая строка печатается дважды?
  • Почему мой scanf("%d", ...)/scanf("%c", ...) не работает?
  • Почему сбой gets()?
  • ...

Ответ помечен как вики сообщества. Не стесняйтесь улучшаться и (осторожно) расширяться.

Ответ 1

Начальный вкладыш для начинающих C

  • Текстовый режим и двоичный режим
  • Отметьте fopen() для отказа
  • Ловушки
    • Проверьте любые функции, которые вы вызываете для успеха
    • EOF, или "почему последняя строка печатается дважды"
    • Не используйте получает(), когда-либо
    • Не используйте * scanf() для потенциально искаженного ввода
    • Когда * scanf() не работает должным образом
  • Прочитайте, затем выполните разбор
    • Прочитайте (часть) строку ввода через fgets()
    • Разбор строки в памяти
  • Очистка

Текстовый режим и двоичный режим

Поток "двоичного режима" считывается точно так же, как он был записан. Однако в конце потока может быть (или может и не быть) количество символов с нулевым значением ('\0'), определенное реализацией.

В текстовом режиме поток может выполнять ряд преобразований, включая (но не ограничиваясь ими):

  • удаление пробелов непосредственно перед концом строки;
  • изменение новых строк ('\n') на что-то другое на выходе (например, "\r\n" в Windows) и обратно на '\n' на входе;
  • добавление, изменение или удаление символов, которые не являются символами печати (isprint( c ) == true), горизонтальными вкладками или новыми строками.

Очевидно, что текстовый и двоичный режимы не смешиваются. Откройте текстовые файлы в текстовом режиме и двоичные файлы в двоичном режиме.

Отметьте fopen() для отказа

Попытка открыть файл может быть неудачной по разным причинам - отсутствие разрешений или файл, который не найден, является наиболее распространенным. В этом случае fopen() вернет указатель NULL.

Он может установить глобальную переменную errno, значение которой можно превратить в сообщение с текстовым сообщением с использованием perror(); это требование POSIX, а не язык C, поэтому он может не работать на каждой платформе.

#include <stdio.h>
#include <errno.h>

int main()
{
    errno = 0;
    FILE * fp = fopen( "file.txt", "rb" );
    if ( fp != NULL )
    {
        // ready to read
    }
    else
    {
        // If supported by fopen(), will print a message
        // *why* fopen() failed, exactly.
        perror( "fopen() failed" );
    }
    fclose( fp );
}

Ловушки

Проверить любые функции, которые вы вызываете для успеха

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

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

EOF, или "почему последняя строка печатается дважды"

Функция feof (поток FILE *) возвращает true, если EOF был достигнут. Непонимание того, что означает "достижение" EOF, означает, что многие новички пишут что-то вроде этого:

// BROKEN CODE
while ( ! feof( fp ) )
{
    fgets( buffer, BUFFER_SIZE, fp );
    puts( buffer );
}

Это приводит к тому, что последняя строка ввода печатается дважды, потому что, когда считывается последняя строка (до последней строки новой строки, последний символ во входном потоке), EOF устанавливается не.

EOF устанавливается только при попытке прочитать последний символ!

Итак, код выше петли еще раз, fgets() не читает другую строку, устанавливает EOF и оставляет содержимое buffer нетронутый, который затем снова печатается.

Итак, после чтения прочитайте feof(), но перед обработкой:

// GOOD CODE
while ( fgets( buffer, BUFFER_SIZE, fp ) != NULL )
{
    puts( buffer );
}

Не используйте получает(),

Невозможно использовать эту функцию безопасно. Из-за этого она была удалена с языка с появлением C11.

Не используйте * scanf() для потенциально некорректного ввода

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

Но цель * scanf() - действительно читать массивные данные, которые могут быть несколько опираться на то, что они находятся в предопределенном формате. (Например, написано другой программой.)

Даже тогда * scanf() может отключить unobservant:

  • Использование строки формата, которая каким-то образом может быть подвергнута влиянию пользователя, - это уязвимое отверстие безопасности.
  • Если вход не соответствует ожидаемому формату, * scanf() немедленно прекращает синтаксический анализ, оставляя все оставшиеся аргументы неинициализированными.
  • Он расскажет вам, сколько назначений успешно выполнено - вот почему вы должны проверить его код возврата (см. выше) - но не там, где именно он прекратил разбор ввода, делая изящные ошибка восстановления затруднительна.
  • Он пропускает любые ведущие пробелы на входе, за исключением случаев, когда это не происходит ([, c и n). (См. Следующий параграф.)
  • Он имеет несколько своеобразное поведение в некоторых случаях.

Когда * scanf() работает не так, как ожидалось

Частая проблема с * scanf() - это когда есть непрочитанные пробелы (' ', '\n',... ) во входном потоке, который пользователь не учитывал.

Чтение числа ("%d" и др.) или строки ("%s") останавливается в любом пробеле. И хотя большинство спецификаторов преобразования *scanf() пропускают ведущие пробелы во вводе, [, c и n этого не делают. Таким образом, новая строка по-прежнему остается первым ожидающим символом ввода, что делает невозможным соответствие %c и %[.

Вы можете пропустить новую строку на входе, явно прочитав ее, например. через fgetc() или добавив пробел к вашему * scanf(). (Единственное пробел в строке формата соответствует любому количеству пробелов во входном файле.)

Однако не вызывать fflush() в вашем потоке ввода, если вы намереваетесь оставаться переносимым. Это хорошо определено только для платформ POSIX; в plain C, вызывая fflush() на входном потоке undefined.

Прочитайте, затем выполните разбор

Мы просто советовали использовать * scanf(), за исключением случаев, когда вы действительно положительно знаете, что делаете. Итак, что использовать в качестве замены?

Вместо чтения и разбора ввода за один раз, когда * scanf() пытается сделать, отделите шаги.

Прочитайте (часть) строку ввода через fgets()

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

Разбор строки в памяти

Особенно полезно для синтаксического анализа в памяти strtol() и strtod(), которые обеспечивают аналогичную функциональность * scanf() спецификаторы преобразования d, i, u, o, x, a, e, f и g.

Но они также сообщают вам, где именно они перестали разбираться, и имеют значительную обработку чисел, слишком больших для целевого типа.

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

И если все остальное не удастся, у вас есть целая строка для печати полезного сообщения об ошибке для пользователя.

Очистка

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

fclose( fp );
fclose( fp );