Что я могу использовать для преобразования ввода вместо scanf?

Я очень часто видел, как люди отговаривали других от использования scanfи говорят, что есть альтернативы получше. Однако все, что я вижу, - это либо "не использоватьscanf" или " здесь правильная строка формата ", и никогда не упоминались примеры " лучших альтернатив ".

Например, возьмем этот фрагмент кода:

scanf("%c", &c);

Читает пробелы, оставшиеся во входном потоке после последнего преобразования. Обычное предлагаемое решение - использовать:

scanf(" %c", &c);

или не использовать scanf.

поскольку scanf плохо, каковы некоторые варианты ANSI C для преобразования форматов ввода, которые scanf обычно может обрабатывать (например, целые числа, числа с плавающей запятой и строки) без использования scanf?

9 ответов

Наиболее распространенные способы чтения ввода:

  • с помощью fgets с фиксированным размером, который обычно предлагается, и

  • с помощью fgetc, что может быть полезно, если вы читаете только один char.

Чтобы преобразовать ввод, вы можете использовать множество функций:

  • strtoll, чтобы преобразовать строку в целое число

  • strtof/d/ld, чтобы преобразовать строку в число с плавающей запятой

  • sscanf, что не так плохо, как простое использованиеscanf, хотя у него есть большинство недостатков, упомянутых ниже

  • Нет хороших способов синтаксического анализа ввода, разделенного разделителями, в простом ANSI C. Либо используйте strtok_r из POSIX или strtok, что не является потокобезопасным. Вы также можете создать свой собственный потокобезопасный вариант, используяstrcspn а также strspn, как strtok_r не требует специальной поддержки ОС.

  • Это может быть излишним, но вы можете использовать лексеры и парсеры (flex а также bison являясь наиболее распространенными примерами).

  • Без преобразования, просто используйте строку


Поскольку я не вдавался в подробности, почему scanf плохо в моем вопросе, я уточню:

  • Со спецификаторами преобразования %[...] а также %c, scanfне съедает пробелы. Очевидно, это малоизвестно, о чем свидетельствует множество дубликатов этого вопроса.

  • Есть некоторая путаница в том, когда использовать унарный & оператор при обращении к scanfаргументы (особенно со строками).

  • Очень легко игнорировать возвращаемое значение из scanf. Это может легко вызвать неопределенное поведение при чтении неинициализированной переменной.

  • Очень легко забыть предотвратить переполнение буфера в scanf. scanf("%s", str) так же плохо, если не хуже, чем gets.

  • Вы не можете обнаружить переполнение при преобразовании целых чисел с помощью scanf. Фактически, переполнение вызывает неопределенное поведение в этих функциях.


Почему scanf Плохо?

Основная проблема в том, что scanfникогда не предназначался для обработки пользовательского ввода. Он предназначен для использования с "идеально" отформатированными данными. Я процитировал слово "идеально", потому что это не совсем так. Но он не предназначен для анализа данных, которые столь же ненадежны, как вводимые пользователем. По своей природе пользовательский ввод непредсказуем. Пользователи неправильно понимают инструкции, делают опечатки, случайно нажимают клавишу ввода, прежде чем они будут выполнены и т. Д. Можно резонно спросить, почему функция, которую не следует использовать для ввода данных пользователем, читает изstdin. Если вы опытный пользователь * nix, объяснение не станет неожиданностью, но может запутать пользователей Windows. В системах * nix очень часто создаются программы, работающие через конвейер, что означает, что вы отправляете вывод одной программы в другую, передаваяstdout первой программы к stdinвторой. Таким образом, вы можете быть уверены, что вывод и ввод предсказуемы. В этих обстоятельствахscanfна самом деле хорошо работает. Но работая с непредсказуемым вводом, вы рискуете столкнуться с множеством неприятностей.

Так почему же нет простых в использовании стандартных функций для пользовательского ввода? Здесь можно только догадываться, но я предполагаю, что старые хардкорные хакеры C просто думали, что существующие функции достаточно хороши, хотя они очень неуклюжи. Кроме того, если вы посмотрите на типичные терминальные приложения, они очень редко читают пользовательский ввод изstdin. Чаще всего вы передаете весь пользовательский ввод как аргументы командной строки. Конечно, есть исключения, но для большинства приложений пользовательский ввод - вещь очень незначительная.

Так что ты можешь сделать?

Прежде всего, getsНЕ является альтернативой. Это опасно и НИКОГДА не должно использоваться. Прочтите здесь, почему: Почему функция gets настолько опасна, что ее не следует использовать?

Мой любимый fgets в комбинации с sscanf. Однажды я написал об этом ответ, но я повторно опубликую полный код. Вот пример с приличной (но не идеальной) проверкой ошибок и синтаксическим анализом. Этого достаточно для отладки.

Запись

Мне не очень нравится просить пользователя ввести две разные вещи в одной строке. Я делаю это только тогда, когда они естественным образом принадлежат друг другу. Как напримерprintf("Enter the price in the format <dollars>.<cent>: ") а затем используйте sscanf(buffer "%d.%d", &dollar, &cent). Я бы никогда не сделал что-то вродеprintf("Enter height and base of the triangle: "). Суть использованияfgets ниже - инкапсулировать входы, чтобы гарантировать, что один вход не влияет на следующий.

#define bsize 100

void error_function(const char *buffer, int no_conversions) {
        fprintf(stderr, "An error occurred. You entered:\n%s\n", buffer);
        fprintf(stderr, "%d successful conversions", no_conversions);
        exit(EXIT_FAILURE);
}

char c, buffer[bsize];
int x,y;
float f, g;
int r;

printf("Enter two integers: ");
fflush(stdout); // Make sure that the printf is executed before reading
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Unless the input buffer was to small we can be sure that stdin is empty
// when we come here.
printf("Enter two floats: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);

// Reading single characters can be especially tricky if the input buffer
// is not emptied before. But since we're using fgets, we're safe.
printf("Enter a char: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%c", &c)) != 1) error_function(buffer, r);

printf("You entered %d %d %f %c\n", x, y, f, c);

Если вы делаете их много, я мог бы порекомендовать создать оболочку, которая всегда сбрасывается:

int printfflush (const char *format, ...)
{
   va_list arg;
   int done;
   va_start (arg, format);
   done = vfprintf (stdout, format, arg);
   fflush(stdout);
   va_end (arg);
   return done;
}

Это устранит общую проблему, а именно завершающую новую строку, которая может испортить ввод вложенности. Но есть еще одна проблема: если линия длиннее, чемbsize. Вы можете проверить это с помощьюif(buffer[strlen(buffer)-1] != '\n'). Если вы хотите удалить новую строку, вы можете сделать это с помощьюbuffer[strcspn(buffer, "\n")] = 0.

В общем, я бы посоветовал не ожидать, что пользователь введет ввод в каком-то странном формате, который вы должны анализировать на разные переменные. Если вы хотите присвоить переменныеheight а также width, не просите и то, и другое одновременно. Разрешите пользователю нажимать ввод между ними. Кроме того, в каком-то смысле такой подход очень естественен. Вы никогда не получите ввод отstdinпока вы не нажмете Enter, так почему бы всегда не читать всю строку? Конечно, это может привести к проблемам, если линия длиннее буфера. Я не забыл упомянуть, что пользовательский ввод в C неуклюжий?:)

Чтобы избежать проблем с строками длиннее буфера, вы можете использовать функцию, которая автоматически выделяет буфер подходящего размера, вы можете использовать getline(). Недостаток в том, что вам нужно будетfree результат потом.

Активизация игры

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

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

ИМО, вот самые большие проблемы с scanf:

  • Риск переполнения буфера - если вы не укажете ширину поля для%s а также %[спецификаторы преобразования, вы рискуете переполнить буфер (пытаясь прочитать больше ввода, чем размер буфера для хранения). К сожалению, нет хорошего способа указать это в качестве аргумента (как сprintf) - вам нужно либо жестко закодировать его как часть спецификатора преобразования, либо выполнить некоторые макросы.

  • Принимает вводы, которые следует отклонить - если вы читаете ввод с%d спецификатор преобразования, и вы вводите что-то вроде 12w4, вы ожидаете scanf отклонить этот ввод, но это не так - он успешно преобразует и назначает 12, уходя w4 во входном потоке, чтобы засорить следующее чтение.

Итак, что вы должны использовать вместо этого?

Обычно я рекомендую читать весь интерактивный ввод как текст, используяfgets - он позволяет указать максимальное количество символов для чтения за раз, поэтому вы можете легко предотвратить переполнение буфера:

char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
  // error reading from input stream, handle as appropriate
}
else
{
  // process input buffer
}

Одна причуда fgets заключается в том, что он сохранит завершающую новую строку в буфере, если есть место, поэтому вы можете легко проверить, ввел ли кто-то больше ввода, чем вы ожидали:

char *newline = strchr( input, '\n' );
if ( !newline )
{
  // input longer than we expected
}

Как вы с этим справитесь, зависит от вас - вы можете либо отклонить весь ввод сразу, либо проглотить любой оставшийся ввод с помощью getchar:

while ( getchar() != '\n' ) 
  ; // empty loop

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

Чтобы токенизировать ввод (разделить его на основе одного или нескольких разделителей), вы можете использоватьstrtok, но будьте осторожны - strtokизменяет свой ввод (он перезаписывает разделители с указателем конца строки), и вы не можете сохранить его состояние (то есть вы не можете частично токенизировать одну строку, затем начать токенизировать другую, а затем продолжить с того места, где вы остановились в исходной строке). Есть вариант,strtok_s, который сохраняет состояние токенизатора, но AFAIK его реализация не является обязательной (вам нужно проверить, что __STDC_LIB_EXT1__ определяется, чтобы узнать, доступен ли он).

После токенизации ввода, если вам нужно преобразовать строки в числа (т. Е. "1234" => 1234), у вас есть варианты. strtol а также strtodпреобразует строковые представления целых и действительных чисел в соответствующие типы. Они также позволяют ловить12w4вопрос, который я упоминал выше - один из их аргументов является указателем на первый символ не преобразуется в строку:

char *text = "12w4";
char *chk;
long val;
long tmp = strtol( text, &chk, 10 );
if ( !isspace( *chk ) && *chk != 0 )
  // input is not a valid integer string, reject the entire input
else
  val = tmp;

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

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

Вот основной рецепт вызова fgets чтобы прочитать строку текста:

char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);

Это просто считывает одну строку текста и печатает ее обратно. Как написано, у него есть пара ограничений, о которых мы поговорим через минуту. У него также есть очень замечательная функция: число 512, которое мы передали в качестве второго аргумента функцииfgets это размер массиваline мы спрашиваем fgetsчитать. Этот факт - что мы можем сказатьfgets сколько можно читать - значит, мы можем быть уверены, что fgets не будет переполнять массив, читая в него слишком много.

Итак, теперь мы знаем, как читать строку текста, но что, если мы действительно хотим прочитать целое число, или число с плавающей запятой, или отдельный символ, или отдельное слово? (То есть что, еслиscanf вызов, который мы пытаемся улучшить, использовал спецификатор формата, например %d, %f, %c, или %s?)

Строку текста - строку - легко интерпретировать как любую из этих вещей. Чтобы преобразовать строку в целое число, самый простой (хотя и несовершенный) способ сделать это - вызватьatoi(). Чтобы преобразовать в число с плавающей запятой, естьatof(). (И есть способы получше, как мы увидим через минуту.) Вот очень простой пример:

printf("type an integer:\n");
fgets(line, 512, stdin);
int i = atoi(line);
printf("type a floating-point number:\n");
fgets(line, 512, stdin);
float f = atof(line);
printf("you typed %d and %f\n", i, f);

Если вы хотите, чтобы пользователь вводил один символ (возможно, y илиn в качестве ответа да / нет), вы можете буквально просто взять первый символ строки, например:

printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);

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

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

hello world!

как строка "hello" за которым следует что-то еще (это то, что scanf формат %s мог бы сделать), ну, в таком случае, я немного накосячил, в конце концов, не так-то просто переинтерпретировать строку таким образом, поэтому ответ на эту часть вопроса придется немного подождать.

Но сначала я хочу вернуться к трем вещам, которые я пропустил.

(1) Мы звонили

fgets(line, 512, stdin);

читать в массив line, а 512 - размер массива line так fgetsзнает, чтобы не переполнять его. Но чтобы убедиться, что 512 - правильное число (особенно, чтобы проверить, не изменил ли кто-то программу, чтобы изменить размер), вам нужно прочитать в любом местеlineбыло объявлено. Это неприятно, поэтому есть два более эффективных способа синхронизировать размеры. Вы можете: (а) использовать препроцессор, чтобы указать размер:

#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);

Или (б) используйте C sizeof оператор:

fgets(line, sizeof(line), stdin);

(2) Вторая проблема заключается в том, что мы не проверяли наличие ошибок. Когда вы читаете ввод, вы всегда должны проверять возможность ошибки. Если по какой-то причинеfgetsне может прочитать запрошенную строку текста, он указывает на это, возвращая нулевой указатель. Итак, мы должны были делать такие вещи, как

printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
    printf("Well, never mind, then.\n");
    exit(1);
}

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

printf("you typed: \"%s\"\n", line);

Если я запускаю это и набираю "Стив", когда он запрашивает меня, он распечатывает

you typed: "Steve
"

Что " во второй строке потому, что строка, которую он прочитал и распечатал, на самом деле "Steve\n".

Иногда этот дополнительный перевод строки не имеет значения (например, когда мы звонилиatoi или atof, поскольку они оба игнорируют любой дополнительный нечисловой ввод после числа), но иногда это имеет большое значение. Очень часто мы хотим убрать эту новую строку. Есть несколько способов сделать это, о которых я расскажу через минуту. (Я знаю, что много говорил об этом. Но я вернусь ко всем этим вещам, я обещаю.)

В этот момент вы можете подумать: "Я думал, вы сказали scanfбыло бесполезно, и этот другой способ был бы намного лучше. Ноfgetsначинает казаться неприятностью. Вызовscanfбыло так просто! Разве я не могу продолжать его использовать?"

Конечно, вы можете продолжать использовать scanf, если хочешь. (А для действительно простых вещей это в некотором смысле проще.) Но, пожалуйста, не приходите ко мне с слезами, когда он подводит вас из-за одной из 17 причуд и недостатков или заходит в бесконечный цикл из-за ввода вашего не ожидали, или когда вы не можете понять, как с его помощью сделать что-то более сложное. И давайте посмотрим наfgetsнастоящие неприятности:

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

  2. Вы должны проверить возвращаемое значение. На самом деле это стирка, потому что использоватьscanf правильно, вы также должны проверить его возвращаемое значение.

  3. Вы должны раздеть \nотвали. Я признаю, что это настоящая неприятность. Хотел бы я указать вам на стандартную функцию, в которой не было бы этой маленькой проблемы. (Пожалуйста, не поднимайтеgets.) Но по сравнению с scanf's 17 разных неприятностей, я возьму на себя эту неприятность fgets любой день.

Так как же убрать новую строку? Три способа:

(а) Очевидный способ:

char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';

(б) Хитрый и компактный способ:

strtok(line, "\n");

К сожалению, это не всегда работает.

(в) Еще один компактный и слегка непонятный способ:

line[strcspn(line, "\n")] = '\0';

И теперь, когда это решено, мы можем вернуться к другой вещи, которую я пропустил: несовершенство atoi() а также atof(). Проблема с ними в том, что они не дают вам никакого полезного указания на успех или неудачу: они незаметно игнорируют конечный нечисловой ввод и тихо возвращают 0, если числовой ввод отсутствует вообще. Предпочтительными альтернативами, которые также имеют некоторые другие преимущества, являются:strtol а также strtod.strtol также позволяет использовать базу, отличную от 10, что означает, что вы можете получить эффект (среди прочего) %o или %x с участием scanf. Но демонстрация того, как правильно использовать эти функции, - это сама по себе история, и это будет слишком отвлекать от того, что уже превращается в довольно фрагментарный рассказ, поэтому я не собираюсь сейчас говорить о них больше.

Остальная часть основного повествования касается ввода, который вы, возможно, пытаетесь разобрать, более сложный, чем просто одно число или символ. Что, если вы хотите прочитать строку, содержащую два числа, или несколько слов, разделенных пробелами, или конкретную пунктуацию кадра? Вот где все становится интересно, и где все, вероятно, усложнялось, если вы пытались делать что-то, используяscanf, и где теперь гораздо больше возможностей, когда вы четко прочитали одну строку текста, используя fgets, хотя полный рассказ обо всех этих вариантах, вероятно, может занять целую книгу, поэтому мы собираемся здесь лишь поцарапать поверхность.

  1. Моя любимая техника - разбить строку на "слова", разделенные пробелами, а затем проделать что-нибудь с каждым "словом". Одна из основных стандартных функций для этого -strtok(который также имеет свои проблемы, и который также заслуживает отдельного обсуждения). Мое собственное предпочтение - это специальная функция для построения массива указателей на каждое разорванное "слово", функция, которую я описываю в этих заметках курса. В любом случае, когда у вас есть "слова", вы можете обрабатывать каждое из них, возможно, с тем жеatoi/atof/strtol/strtodфункции, которые мы уже рассмотрели.

  2. Как это ни парадоксально, хотя мы потратили немало времени и усилий на то, чтобы понять, как отойти от scanf, еще один отличный способ работать со строкой текста, которую мы только что прочитали,fgets передать это sscanf. Таким образом, вы получаете большинство преимуществscanf, но без большинства недостатков.

  3. Если ваш синтаксис ввода особенно сложен, может быть уместно использовать библиотеку "regexp" для его анализа.

  4. Наконец, вы можете использовать любые подходящие вам решения специального анализа. Вы можете перемещаться по строке по символу за раз с помощьюchar *проверка указателя на ожидаемые символы. Или вы можете искать определенные символы, используя такие функции, какstrchr или strrchr, или strspn или strcspn, или strpbrk. Или вы можете анализировать / преобразовывать и пропускать группы цифровых символов, используяstrtol илиstrtod функции, которые мы пропустили ранее.

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

Что я могу использовать для синтаксического анализа ввода вместо scanf?

Вместо того scanf(some_format, ...), рассмотреть возможность fgets() с участием sscanf(buffer, some_format_and %n, ...)

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

// scanf("%d %f fred", &some_int, &some_float);
#define EXPECTED_LINE_MAX 100
char buffer[EXPECTED_LINE_MAX * 2];  // Suggest 2x, no real need to be stingy.

if (fgets(buffer, sizeof buffer, stdin)) {
  int n = 0;
  // add ------------->    " %n" 
  sscanf(buffer, "%d %f fred %n", &some_int, &some_float, &n);
  // Did scan complete, and to the end?
  if (n > 0 && buffer[n] == '\0') {
    // success, use `some_int, some_float`
  } else {
    ; // Report bad input and handle desired.
  }

Сформулируем требования к синтаксическому анализу как:

  • действительный ввод должен быть принят (и преобразован в другую форму)

  • недопустимый ввод должен быть отклонен

  • когда какой-либо ввод отклонен, необходимо предоставить пользователю описательное сообщение, в котором объясняется (на ясном языке, "легко понятном нормальным людям, не являющимся программистами"), почему он был отклонен (чтобы люди могли понять, как исправить ошибку проблема)

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

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

Давайте также правильно определим "ввод, содержащий недопустимые символы"; и скажите, что:

  • начальные и конечные пробелы будут игнорироваться (например, "
    5" будет обрабатываться как "5")
  • допускается ноль или одна десятичная точка (например, "1234." и "1234.000" обрабатываются так же, как "1234")
  • должна быть хотя бы одна цифра (например, "." отклоняется)
  • допускается не более одной десятичной точки (например, "1.2.3" отклоняется)
  • запятые, которые не находятся между цифрами, будут отклонены (например, ",1234" отклоняется)
  • запятые после десятичной точки будут отклонены (например, "1234.000,000" отклоняется)
  • запятые, стоящие после другой запятой, отклоняются (например, "1,,234" отклоняется)
  • все остальные запятые будут проигнорированы (например, "1,234" будет рассматриваться как "1234")
  • знак минус, который не является первым непробельным символом, отклоняется
  • положительный знак, который не является первым непробельным символом, отклоняется

Исходя из этого, мы можем определить, что необходимы следующие сообщения об ошибках:

  • "Неизвестный символ в начале ввода"
  • "Неизвестный символ в конце ввода"
  • "Неизвестный символ в середине ввода"
  • "Число слишком мало (минимум....)"
  • "Число слишком велико (максимум....)"
  • "Число не целое"
  • "Слишком много десятичных знаков"
  • "Без десятичных цифр"
  • "Неверная запятая в начале номера"
  • "Неправильная запятая в конце числа"
  • "Неправильная запятая в середине числа"
  • "Неверная запятая после десятичной точки"

С этого момента мы видим, что подходящая функция для преобразования строки в целое число должна различать очень разные типы ошибок; и что-то вроде "scanf()" или "atoi()" или "strtoll()"полностью и совершенно бесполезны, потому что они не могут дать вам никаких указаний на то, что было не так с вводом (и используют совершенно неуместное и неуместное определение того, что является / не является" правильным вводом ").

Вместо этого давайте начнем писать что-нибудь бесполезное:

char *convertStringToInteger(int *outValue, char *string, int minValue, int maxValue) {
    return "Code not implemented yet!";
}

int main(int argc, char *argv[]) {
    char *errorString;
    int value;

    if(argc < 2) {
        printf("ERROR: No command line argument.\n");
        return EXIT_FAILURE;
    }
    errorString = convertStringToInteger(&value, argv[1], -10, 2000);
    if(errorString != NULL) {
        printf("ERROR: %s\n", errorString);
        return EXIT_FAILURE;
    }
    printf("SUCCESS: Your number is %d\n", value);
    return EXIT_SUCCESS;
}

Соответствовать заявленным требованиям; этотconvertStringToInteger() функция, скорее всего, сама по себе превратится в несколько сотен строк кода.

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

Другими словами...

Что я могу использовать для синтаксического анализа ввода вместо scanf?

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

Вот пример использования flex для сканирования простого ввода, в данном случае файла чисел с плавающей запятой ASCII, которые могут быть либо в США (n,nnn.dd) или европейский (n.nnn,dd) форматы. Это просто скопировано из гораздо более крупной программы, поэтому могут быть некоторые неразрешенные ссылки:

/* This scanner reads a file of numbers, expecting one number per line.  It  */
/* allows for the use of European-style comma as decimal point.              */

%{
  #include <stdlib.h>
  #include <stdio.h>
  #include <string.h>
  #ifdef WINDOWS
    #include <io.h>
  #endif
  #include "Point.h"

  #define YY_NO_UNPUT
  #define YY_DECL int f_lex (double *val)

  double atofEuro (char *);
%}

%option prefix="f_"
%option nounput
%option noinput

EURONUM [-+]?[0-9]*[,]?[0-9]+([eE][+-]?[0-9]+)?
NUMBER  [-+]?[0-9]*[\.]?[0-9]+([eE][+-]?[0-9]+)?
WS      [ \t\x0d]

%%

[!@#%&*/].*\n

^{WS}*{EURONUM}{WS}*  { *val = atofEuro (yytext); return (1); }
^{WS}*{NUMBER}{WS}*   { *val = atof (yytext); return (1); }

[\n]
.


%%

/*------------------------------------------------------------------------*/

int scan_f (FILE *in, double *vals, int max)
{
  double *val;
  int npts, rc;

  f_in = in;
  val  = vals;
  npts = 0;
  while (npts < max)
  {
    rc = f_lex (val);

    if (rc == 0)
      break;
    npts++;
    val++;
  }

  return (npts);
}

/*------------------------------------------------------------------------*/

int f_wrap ()
{
  return (1);
}

Одним из наиболее распространенных применений является чтение одиночного ввода от пользователя. Поэтому мой ответ будет сосредоточен только на этой одной проблеме.

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

      int num;

printf( "Please enter an integer: " );

if ( scanf( "%d", &num ) != 1 )
{
    printf( "Error converting input!\n" );
}
else
{
    printf( "The input was successfully converted to %d.\n", num );
}

Использование таким образом имеет несколько проблем:

Функция не всегда будет читать всю строку ввода.

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

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

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

Еще одна проблема с использованием с%dСпецификатор формата заключается в том, что если результат преобразования не может быть представлен в виде (например, если результат больше, чем), то, согласно §7.21.6.2 ¶10 стандарта ISO C11 , поведение программы не определено, а значит, нельзя полагаться на какое-то конкретное поведение.

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

      #include <stdio.h>
#include <stdlib.h>

int main( void )
{
    char line[200], *p;
    int num;

    //prompt user for input
    printf( "Enter a number: " );

    //attempt to read one line of input
    if ( fgets( line, sizeof line, stdin ) == NULL )
    {
        printf( "Input failure!\n" );
        exit( EXIT_FAILURE );
    }

    //attempt to convert string to integer
    num = strtol( line, &p, 10 );
    if ( p == line )
    {
        printf( "Unable to convert to integer!\n" );
        exit( EXIT_FAILURE );
    }

    //print result
    printf( "Conversion successful! The number is %d.\n", num );
}

Однако этот код имеет следующие проблемы:

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

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

  3. Он примет6abcкак допустимый ввод для числа6. Это не так уж и плохо, потому чтоscanfпокинетabcна входном потоке, тогда какfgetsне будет. Тем не менее, вероятно, было бы лучше отклонить ввод, чем принять его.

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

Проблема № 1 может быть решена путем проверки

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

Проблема № 2 может быть решена путем проверки того, установлена ​​ли функцияerrnoк значению константы макросаERANGE, чтобы определить, может ли преобразованное значение быть представлено какlong. Чтобы определить, представимо ли это значение также какint, значение, возвращаемое функцией, должно сравниваться сINT_MINиINT_MAX.

Проблема №3 решается проверкой всех оставшихся символов в строке. Сstrtolпринимает начальные пробельные символы , вероятно, было бы также уместно принимать завершающие пробельные символы. Однако, если ввод содержит какие-либо другие завершающие символы, ввод, вероятно, следует отклонить.

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

      #include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>
#include <errno.h>

int get_int_from_user( const char *prompt )
{
    //loop forever until user enters a valid number
    for (;;)
    {
        char buffer[1024], *p;
        long l;

        //prompt user for input
        fputs( prompt, stdout );

        //get one line of input from input stream
        if ( fgets( buffer, sizeof buffer, stdin ) == NULL )
        {
            fprintf( stderr, "Unrecoverable input error!\n" );
            exit( EXIT_FAILURE );
        }

        //make sure that entire line was read in (i.e. that
        //the buffer was not too small)
        if ( strchr( buffer, '\n' ) == NULL && !feof( stdin ) )
        {
            int c;

            printf( "Line input was too long!\n" );

            //discard remainder of line
            do
            {
                c = getchar();

                if ( c == EOF )
                {
                    fprintf( stderr, "Unrecoverable error reading from input!\n" );
                    exit( EXIT_FAILURE );
                }

            } while ( c != '\n' );

            continue;
        }

        //attempt to convert string to number
        errno = 0;
        l = strtol( buffer, &p, 10 );
        if ( p == buffer )
        {
            printf( "Error converting string to number!\n" );
            continue;
        }

        //make sure that number is representable as an "int"
        if ( errno == ERANGE || l < INT_MIN || l > INT_MAX )
        {
            printf( "Number out of range error!\n" );
            continue;
        }

        //make sure that remainder of line contains only whitespace,
        //so that input such as "6abc" gets rejected
        for ( ; *p != '\0'; p++ )
        {
            if ( !isspace( (unsigned char)*p ) )
            {
                printf( "Unexpected input encountered!\n" );

                //cannot use `continue` here, because that would go to
                //the next iteration of the innermost loop, but we
                //want to go to the next iteration of the outer loop
                goto continue_outer_loop;
            }
        }

        return l;

    continue_outer_loop:
        continue;
    }
}

int main( void )
{
    int number;

    number = get_int_from_user( "Enter a number: " );

    printf( "Input was valid.\n" );
    printf( "The number is: %d\n", number );

    return 0;
}

Эта программа имеет следующее поведение:

      Enter a number: abc
Error converting string to number!
Enter a number: 6000000000
Number out of range error!
Enter a number: 6 7 8
Unexpected input encountered!
Enter a number: 6abc
Unexpected input encountered!
Enter a number: 6
Input was valid.
The number is: 6

Другие ответы дают правильные подробности низкого уровня, поэтому я ограничусь более высоким уровнем: во-первых, проанализируйте, как вы ожидаете, что каждая строка ввода будет выглядеть. Попробуйте описать ввод с помощью формального синтаксиса - если повезет, вы обнаружите, что его можно описать с помощью обычной грамматики или, по крайней мере, контекстно-свободной грамматики. Если обычной грамматики достаточно, вы можете написать код конечного автомата который распознает и интерпретирует каждую командную строку по одному символу за раз. Затем ваш код прочитает строку (как описано в других ответах), а затем просканирует символы в буфере через конечный автомат. В определенных состояниях вы останавливаетесь и конвертируете просканированную подстроку в число или что-то еще. Вы, вероятно, сможете "свернуть свое собственное", если это так просто; если вы обнаружите, что вам нужна полная контекстно-свободная грамматика, вам лучше выяснить, как использовать существующие инструменты синтаксического анализа (re:lex а также yacc или их варианты).

Другие вопросы по тегам