Как читать / анализировать ввод в C? FAQ

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

Помогите?


Это запись часто задаваемых вопросов.

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

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

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

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

1 ответ

Решение

Начальный вводный C-праймер

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

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

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

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

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

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

Проверьте fopen() на неудачу

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

когда fopen терпит неудачу, обычно устанавливает глобальную переменную errno, чтобы указать причину сбоя. (Технически это не является обязательным требованием языка Си, но POSIX и Windows гарантируют это.) errno это кодовый номер, который можно сравнить с константами в errno.h, но в простых программах обычно все, что вам нужно сделать, это превратить его в сообщение об ошибке и распечатать, используя perror() или же strerror(), Сообщение об ошибке также должно включать имя файла, который вы передали fopen; если вы этого не сделаете, вы будете очень смущены, когда проблема в том, что имя файла не соответствует вашему.

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

int main(int argc, char **argv)
{
    if (argc < 2) {
        fprintf(stderr, "usage: %s file\n", argv[0]);
        return 1;
    }

    FILE *fp = fopen(argv[1], "rb");
    if (!fp) {
        // alternatively, just `perror(argv[1])`
        fprintf(stderr, "cannot open %s: %s\n", argv[1], strerror(errno));
        return 1;
    }

    // read from fp here

    fclose(fp);
    return 0;
}

Ловушки

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

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

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

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

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

// BROKEN CODE
while (!feof(fp)) {
    fgets(buffer, BUFFER_SIZE, fp);
    printf("%s", buffer);
}

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

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

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

Вместо этого проверьте, fgets не удалось напрямую:

// GOOD CODE
while (fgets(buffer, BUFFER_SIZE, fp)) {
    printf("%s", buffer);
}

Не используйте get (), никогда

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

Не используйте fflush() на stdin или любой другой поток, открытый для чтения, когда-либо

Многие ожидают fflush(stdin) отменить ввод пользователя, который еще не был прочитан. Это не делает этого. В простом ISO C вызов fflush() для входного потока имеет неопределенное поведение. Он имеет четко определенное поведение в POSIX и MSVC, но ни один из них не заставляет его игнорировать ввод пользователя, который еще не был прочитан.

Как правило, правильный способ очистки ожидающих данных - чтение и удаление символов вплоть до новой строки, но не далее:

int c;
do c = getchar(); while (c != EOF && c != '\n');

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

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

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

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

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

Когда *scanf() не работает должным образом

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

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

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

Читай, потом разбирай

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

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

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

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

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

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

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

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

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

Очистить

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

fclose(fp);
Другие вопросы по тегам