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


125

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

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

scanf("%c", &c);

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

scanf(" %c", &c);

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

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

Ответы:


87

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

  • используя 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. Фактически, переполнение вызывает неопределенное поведение в этих функциях.



56

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

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

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

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

Мой любимый 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пока не нажмете ввод, так почему бы не всегда читать всю строку? Конечно, это может привести к проблемам, если строка длиннее буфера. Я помнил, чтобы упомянуть, что пользовательский ввод неуклюже в C? :)

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

Активизировать игру

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


Обратите внимание, что (r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2не плохо распознает конечный нечисловой текст.
chux - Восстановить Монику

1
@chux Исправлено% f% f. Что вы имеете в виду под первым?
Klutt

С fgets()из "1 2 junk", if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {не сообщает ничего плохого с вводом, даже если он имеет "мусор".
Chux - Восстановить Монику

@ chux Ах, теперь я вижу. Ну, это было сделано намеренно.
Klutt

1
scanfпредназначен для использования с отлично отформатированными данными, но даже это не так. Помимо проблемы с «мусором», как упомянуто @chux, есть также тот факт, что формат, как например "%d %d %d", рад прочитать входные данные из одной, двух или трех строк (или даже больше, если есть промежуточные пустые строки), что нет способ форсировать (скажем) двухстрочный ввод, делая что-то вроде "%d\n%d %d"и т. д., scanfможет быть подходящим для форматированного потока , но это совсем не хорошо для чего-либо, основанного на строках.
Стив Саммит

18

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;

Если вы не укажете ширину поля ... - или подавление преобразования (например %*[%\n], это полезно для работы с слишком длинными строками в ответе).
Тоби Спейт

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

5
Здесь вы допустили наиболее распространенную ошибку isspace()- она ​​принимает неподписанные символы, представленные как int, поэтому вам нужно привести к, unsigned charчтобы избежать UB на платформах, где charподписано.
Тоби Спейт

9

В этом ответе я собираюсь предположить, что вы читаете и интерпретируете строки текста . Возможно, вы предлагаете пользователю, который что-то печатает и нажимает 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);

Или (б) используйте 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's17 различными неприятностями, я возьму это одно неудобство fgetsлюбого дня.

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

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

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

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

strtok(line, "\n");

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

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

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которые мы пропустили ранее.

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


Есть ли хорошая причина, чтобы писать, sizeof (line)а не просто sizeof line? Первый делает это выглядит как lineимя типа!
Тоби Спейт

@TobySpeight Хорошая причина? Нет, я в этом сомневаюсь. Скобки - это моя привычка, потому что я не могу вспомнить, нужны ли им объекты или имена типов, но многие программисты оставляют их, когда могут. (Для меня это вопрос личных предпочтений и стиля, причем довольно незначительный.)
Стив Саммит

+1 за использование sscanfв качестве механизма преобразования, но сбор (и, возможно, массирование) ввода с помощью другого инструмента. Но, возможно, стоит упомянуть getlineв этом контексте.
dmckee --- котенок экс-модератора

Когда вы говорите о « fscanfреальных неприятностях», вы имеете в виду fgets? И неприятность # 3 действительно раздражает меня, особенно учитывая, что scanfвозвращает бесполезный указатель на буфер, а не возвращает количество введенных символов (что сделало бы удаление новой строки намного чище).
суперкат

1
Спасибо за объяснение вашего sizeofстиля. Для меня вспоминать, когда ты нужен парен, легко: я думаю, (type)что я похож на бросок без значения (потому что нас интересует только тип). Еще одна вещь: вы говорите, что strtok(line, "\n")это не всегда работает, но это не очевидно, когда это не так. Я предполагаю, что вы думаете о случае, когда строка была длиннее буфера, поэтому у нас нет новой строки, и strtok()возвращается ноль? Жаль, fgets()что не возвращает более полезного значения, поэтому мы можем знать, есть ли перевод строки или нет.
Тоби Спейт

7

Что я могу использовать для анализа ввода вместо 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.
  }

6

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

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

  • неверный ввод должен быть отклонен

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

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

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

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

  • начальные и конечные пробелы будут игнорироваться (например, «
    5» будет рассматриваться как «5»)
  • допускается ноль или одна десятичная точка (например, «1234» и «1234.000» обрабатываются так же, как «1234»)
  • должна быть хотя бы одна цифра (например, "." отклонено)
  • допускается не более одной десятичной точки (например, «1.2.3» отклонено)
  • запятые, которые не находятся между цифрами, будут отклонены (например, ", 1234" отклонено)
  • запятые после десятичной точки будут отклонены (например, «1234.000.000» отклонено)
  • запятые после другой запятой отклоняются (например, «1, 234» отклоняется)
  • все остальные запятые будут игнорироваться (например, «1234» будет рассматриваться как «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?

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


5

Вот пример использования flexдля сканирования простого ввода, в данном случае это файл чисел с плавающей запятой ASCII, который может быть в формате US ( n,nnn.dd) или European ( 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);
}

-5

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


Конечный автомат может быть излишним; Возможны более простые способы обнаружения переполнения в конверсиях (например, проверка errno == EOVERFLOWпосле использования strtoll).
SS Anne

1
Зачем вам кодировать свой собственный конечный автомат, когда flex делает их написание тривиально простым?
Джеймс
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.