Почему scanf
плохо?
Основная проблема заключается в том, что scanf
он никогда не был предназначен для пользовательского ввода. Он предназначен для использования с «идеально» отформатированными данными. Я процитировал слово «отлично», потому что оно не совсем верно. Но он не предназначен для анализа данных, которые столь же ненадежны, как и пользовательский ввод. По своей природе ввод пользователя не предсказуем. Пользователь неправильно понимает инструкции, делает опечатки, случайно нажимает ввод до того, как они это сделают и т.д. Можно разумно спросить, почему функция, которая не должна использоваться для пользовательского ввода, читает из stdin
. Если вы опытный пользователь * nix, объяснение не будет сюрпризом, но может запутать пользователей Windows. В системах * nix очень распространено создавать программы, которые работают по трубопроводу,stdout
stdin
второй. Таким образом, вы можете убедиться, что вывод и ввод являются предсказуемыми. В этих условиях на scanf
самом деле хорошо работает. Но при работе с непредсказуемым вводом вы рискуете всевозможными неприятностями.
Так почему же нет простых в использовании стандартных функций для пользовательского ввода? Здесь можно только догадываться, но я предполагаю, что старые хардкорные хакеры C просто думали, что существующие функции были достаточно хороши, даже если они очень неуклюжи. Кроме того, когда вы смотрите на типовые терминальные приложения, они очень редко читают пользовательский ввод stdin
. Чаще всего вы передаете весь пользовательский ввод в качестве аргументов командной строки. Конечно, есть исключения, но для большинства приложений пользовательский ввод очень незначительный.
Так что ты можешь сделать?
Мой любимый fgets
в сочетании с sscanf
. Я однажды написал ответ об этом, но я повторно выложу полный код. Вот пример с достойной (но не идеальной) проверкой и анализом ошибок. Это достаточно для отладки.
Запись
Мне не особенно нравится просить пользователя вводить две разные вещи в одну строку. Я делаю это только тогда, когда они естественным образом принадлежат друг другу. Как например, printf("Enter the price in the format <dollars>.<cent>: ")
а затем использовать sscanf(buffer "%d.%d", &dollar, ¢)
. Я бы никогда не сделал что-то подобное 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
не плохо распознает конечный нечисловой текст.fgets()
из"1 2 junk"
,if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {
не сообщает ничего плохого с вводом, даже если он имеет "мусор".scanf
предназначен для использования с отлично отформатированными данными, но даже это не так. Помимо проблемы с «мусором», как упомянуто @chux, есть также тот факт, что формат, как например"%d %d %d"
, рад прочитать входные данные из одной, двух или трех строк (или даже больше, если есть промежуточные пустые строки), что нет способ форсировать (скажем) двухстрочный ввод, делая что-то вроде"%d\n%d %d"
и т. д.,scanf
может быть подходящим для форматированного потока , но это совсем не хорошо для чего-либо, основанного на строках.scanf
это здорово, когда вы знаете, что ваш вклад всегда хорошо структурирован и хорошо себя ведет. В противном случае...ИМО, вот самые большие проблемы с
scanf
:Риск переполнения буфера - если вы не укажете ширину поля для спецификаторов
%s
и%[
преобразования, вы рискуете переполнением буфера (пытаясь прочитать больше входных данных, чем размер буфера для хранения). К сожалению, нет хорошего способа указать это в качестве аргумента (как в случае сprintf
) - вы должны либо жестко закодировать его как часть спецификатора преобразования, либо выполнить некоторые макропрограммы.Принимает входные данные, которые должны быть отклонены. Если вы читаете входные данные с помощью
%d
спецификатора преобразования и вводите что-то вроде этого12w4
, вы можете ожидатьscanf
отклонения этих входных данных, но это не так - он успешно преобразует и присваивает12
, оставляяw4
во входном потоке чтобы запутаться в следующем чтении.Итак, что вы должны использовать вместо этого?
Я обычно рекомендую читать весь интерактивный ввод как текстовый
fgets
- он позволяет указывать максимальное количество символов для чтения за раз, поэтому вы можете легко предотвратить переполнение буфера:Одна из странностей
fgets
заключается в том, что он будет хранить завершающий символ новой строки в буфере, если есть место, поэтому вы можете легко проверить, набрал ли кто-то больше ввода, чем вы ожидали:То, как вы справляетесь с этим, зависит от вас - вы можете либо отказаться от всего ввода из-под контроля, либо отобрать любой оставшийся ввод с помощью
getchar
:Или вы можете обработать введенные данные и прочитать снова. Это зависит от проблемы, которую вы пытаетесь решить.
Чтобы токенизировать входные данные (разделить их на основе одного или нескольких разделителей), вы можете использовать
strtok
, но будьте осторожны -strtok
изменяете его входные данные (он перезаписывает разделители с помощью ограничителя строки), и вы не можете сохранить его состояние (т. Е. Вы можете ' • частично токенизировать одну строку, затем начать токенизировать другую, а затем продолжить с того места, где вы остановились в исходной строке). Есть вариант,strtok_s
который сохраняет состояние токенизатора, но AFAIK его реализация необязательна (вам нужно проверить, что__STDC_LIB_EXT1__
определено, чтобы увидеть, доступен ли он).После того как вы введете токены, если вам нужно преобразовать строки в числа (т. Е.
"1234"
=>1234
), У вас есть варианты.strtol
иstrtod
преобразует строковые представления целых и действительных чисел в соответствующие им типы. Они также позволяют вам уловить12w4
проблему, о которой я упоминал выше - один из аргументов - указатель на первый символ, не преобразованный в строку:источник
%*[%\n]
, это полезно для работы с слишком длинными строками в ответе).snprintf()
),.isspace()
- она принимает неподписанные символы, представленные какint
, поэтому вам нужно привести к,unsigned char
чтобы избежать UB на платформах, гдеchar
подписано.В этом ответе я собираюсь предположить, что вы читаете и интерпретируете строки текста . Возможно, вы предлагаете пользователю, который что-то печатает и нажимает RETURN. Или, возможно, вы читаете строки структурированного текста из какого-либо файла данных.
Поскольку вы читаете строки текста, имеет смысл организовать ваш код вокруг библиотечной функции, которая читает, ну, в общем, строку текста. Стандартная функция есть
fgets()
, хотя есть и другие (в том числеgetline
). И затем следующий шаг - как-то интерпретировать эту строку текста.Вот основной рецепт для вызова,
fgets
чтобы прочитать строку текста:Это просто читает в одной строке текста и печатает его обратно. Как написано, у него есть пара ограничений, которые мы получим через минуту. У него также есть очень хорошая особенность: число 512, которое мы передали в качестве второго аргумента,
fgets
является размером массива, в которыйline
мы просимfgets
прочитать. Этот факт - то, что мы можем сказать,fgets
сколько ему разрешено читать, - означает, что мы можем быть уверены, чтоfgets
массив не переполнится, если читать слишком много в него.Итак, теперь мы знаем, как читать строку текста, но что, если мы действительно хотим прочитать целое число, или число с плавающей запятой, или один символ, или одно слово? (То есть, что делать , если
scanf
вызов мы пытаемся улучшить был использованием спецификатора формата , как%d
,%f
,%c
или%s
?)Легко переосмыслить строку текста - строку - как любую из этих вещей. Чтобы преобразовать строку в целое число, самый простой (хотя и несовершенный) способ сделать это - вызвать
atoi()
. Чтобы преобразовать в число с плавающей точкой, естьatof()
. (И есть и лучшие способы, как мы увидим через минуту.) Вот очень простой пример:Если вы хотите, чтобы пользователь вводил один символ (возможно,
y
илиn
как ответ «да / нет»), вы можете буквально просто захватить первый символ строки, например так:(Это, конечно, игнорирует возможность того, что пользователь набрал многосимвольный ответ; он тихо игнорирует любые дополнительные символы, которые были набраны.)
Наконец, если вы хотите, чтобы пользователь набрал строку, определенно не содержащую пробелов, если вы хотите обработать строку ввода
поскольку за строкой
"hello"
следует что-то еще (что и сделал быscanf
формат%s
), ну, в этом случае, я немного выдумал, в конце концов, не так просто интерпретировать строку таким образом, так что ответ на этот вопрос Часть вопроса придется немного подождать.Но сначала я хочу вернуться к трем вещам, которые я пропустил.
(1) мы звонили
читать в массив
line
, и где 512 - размер массива,line
поэтомуfgets
знает, что не переполнить его. Но чтобы убедиться, что 512 - это правильное число (особенно, чтобы проверить, возможно, кто-то подправил программу, чтобы изменить размер), вы должны вернуться туда, гдеline
было объявлено. Это неприятно, поэтому есть два гораздо лучших способа синхронизировать размеры. Вы можете, (а) использовать препроцессор, чтобы сделать имя для размера:Или (б) используйте
sizeof
оператор С:(2) Вторая проблема заключается в том, что мы не проверяли ошибки. Когда вы читаете ввод, вы всегда должны проверять возможность ошибки. Если по какой-либо причине
fgets
не удается прочитать строку текста, к которой вы его просили, это указывает на это, возвращая нулевой указатель. Таким образом, мы должны были делать такие вещи, какНаконец, существует проблема, заключающаяся в том, что для того, чтобы прочитать строку текста, прочитать
fgets
символы и заполнить их в вашем массиве, пока не будет найден\n
символ, заканчивающий строку, и он также заполнит\n
символ в вашем массиве . Это можно увидеть, если немного изменить наш предыдущий пример:Если я запускаю это и набираю «Стив», когда он мне подсказывает, он печатает
Это
"
во второй строке, потому что строка, которую он прочитал и распечатал, была на самом деле"Steve\n"
.Иногда этот дополнительный символ новой строки не имеет значения (например, когда мы звонили
atoi
илиatof
, поскольку они оба игнорируют любой дополнительный нечисловой ввод после числа), но иногда это имеет большое значение. Так часто мы хотим убрать эту новую строку. Есть несколько способов сделать это, к которым я вернусь через минуту. (Я знаю, что много говорил. Но я вернусь ко всем этим вещам, обещаю.)В этот момент вы можете подумать: «Я думал, что вы сказали, что
scanf
это не хорошо, и этот другой способ был бы намного лучше. Ноfgets
он начинает выглядеть как неприятность. Звонитьscanf
было так легко ! Разве я не могу продолжать его использовать? "Конечно, вы можете продолжать использовать
scanf
, если хотите. (И для действительно простых вещей, в некотором смысле, это проще.) Но, пожалуйста, не приходите ко мне плакать, когда он подведет вас из-за одной из 17 его причуд и слабостей, или войдет в бесконечный цикл из-за ввода вашего не ожидал, или когда вы не можете понять, как использовать это, чтобы сделать что-то более сложное. И давайте посмотрим наfgets
реальные неприятности:Вы всегда должны указывать размер массива. Ну, конечно, это совсем не неприятность - это особенность, потому что переполнение буфера - это действительно плохо.
Вы должны проверить возвращаемое значение. На самом деле, это стирка, потому что для
scanf
правильного использования , вы также должны проверить его возвращаемое значение.Вы должны раздеться
\n
. Это, я признаю, настоящая неприятность. Хотелось бы, чтобы была стандартная функция, на которую я мог бы указать, у которой не было этой маленькой проблемы. (Пожалуйста, никто не воспитываетgets
.) Но по сравнению сscanf's
17 различными неприятностями, я возьму это одно неудобствоfgets
любого дня.Так как же раздеть эту новую строку? Три способа:
(а) Очевидный способ:
(б) хитрый и компактный способ:
К сожалению, этот не всегда работает.
(c) Еще один компактный и слегка неясный способ:
И теперь, когда это не так, мы можем вернуться к другой вещи, которую я пропустил: недостатки
atoi()
иatof()
. Проблема в том, что они не дают вам полезного указания на успех или неудачу: они тихо игнорируют завершающий нечисловой ввод и спокойно возвращают 0, если числовой ввод вообще отсутствует. Предпочтительными альтернативами, которые также имеют определенные другие преимущества, являютсяstrtol
иstrtod
.strtol
также позволяет использовать базу, отличную от 10, то есть вы можете получить эффект (среди прочего)%o
или%x
сscanf
, Но показ того, как правильно использовать эти функции, сам по себе является историей, и это слишком отвлекает от того, что уже превращается в довольно фрагментированный рассказ, так что я не буду сейчас говорить о них больше.Остальная часть основного повествования касается ввода, который вы, возможно, пытаетесь разобрать, это более сложный, чем просто одно число или символ. Что если вы хотите прочитать строку, содержащую два числа, или несколько слов, разделенных пробелами, или специальную пунктуацию? Вот где все становится интересным, и где, возможно, все усложняется, если вы пытаетесь сделать что-то с помощью
scanf
, и где теперь гораздо больше вариантов, когда вы чисто прочитали одну строку текста, используяfgets
, хотя полная история всех этих вариантов Возможно, мы могли бы заполнить книгу, поэтому мы только сможем здесь поцарапать поверхность.Моя любимая техника - разбить строку на «слова», разделенные пробелами, а затем сделать что-то еще с каждым «словом». Одной из основных стандартных функций для этого является
strtok
(которая также имеет свои проблемы и которая также оценивает отдельное обсуждение). Мое собственное предпочтение - выделенная функция для построения массива указателей на каждое разбитое на части «слово», функция, которую я описываю в этих заметках к курсу . Во всяком случае, как только вы получили «слова», вы можете дополнительно обработать каждый из них, возможно , с теми жеatoi
/atof
/strtol
/strtod
функциями мы уже смотрели.Как это ни парадоксально, даже несмотря на то, что мы потратили немало времени и усилий, чтобы выяснить, как отойти от этого
scanf
, еще один прекрасный способ справиться с только что прочитанной строкой текстаfgets
- передать ееsscanf
. Таким образом, вы получите большинство преимуществscanf
, но без большинства недостатков.Если ваш входной синтаксис особенно сложен, может быть целесообразно использовать библиотеку "regexp" для его анализа.
Наконец, вы можете использовать любые специальные решения для анализа. Вы можете перемещаться по строке на символ за раз с помощью
char *
указателя, который проверяет наличие ожидаемых символов. Или вы можете искать конкретные символы, используя такие функции, какstrchr
илиstrrchr
, илиstrspn
илиstrcspn
, илиstrpbrk
. Или вы можете анализировать / преобразовывать и пропускать группы цифровых символов, используя функцииstrtol
или,strtod
которые мы пропустили ранее.Очевидно, можно сказать гораздо больше, но, надеюсь, это введение поможет вам начать.
источник
sizeof (line)
а не простоsizeof line
? Первый делает это выглядит какline
имя типа!sscanf
в качестве механизма преобразования, но сбор (и, возможно, массирование) ввода с помощью другого инструмента. Но, возможно, стоит упомянутьgetline
в этом контексте.fscanf
реальных неприятностях», вы имеете в видуfgets
? И неприятность # 3 действительно раздражает меня, особенно учитывая, чтоscanf
возвращает бесполезный указатель на буфер, а не возвращает количество введенных символов (что сделало бы удаление новой строки намного чище).sizeof
стиля. Для меня вспоминать, когда ты нужен парен, легко: я думаю,(type)
что я похож на бросок без значения (потому что нас интересует только тип). Еще одна вещь: вы говорите, чтоstrtok(line, "\n")
это не всегда работает, но это не очевидно, когда это не так. Я предполагаю, что вы думаете о случае, когда строка была длиннее буфера, поэтому у нас нет новой строки, иstrtok()
возвращается ноль? Жаль,fgets()
что не возвращает более полезного значения, поэтому мы можем знать, есть ли перевод строки или нет.Вместо того
scanf(some_format, ...)
, чтобы рассмотретьfgets()
сsscanf(buffer, some_format_and %n, ...)
С помощью
" %n"
кода можно просто определить, все ли форматы были успешно отсканированы, и что в конце не было лишнего мусора без пробелов.источник
Давайте сформулируем требования разбора как:
действительный ввод должен быть принят (и преобразован в другую форму)
неверный ввод должен быть отклонен
когда любой ввод отклонен, необходимо предоставить пользователю описательное сообщение, которое объясняет (понятным языком «легко понимаемый обычными людьми, не являющимися программистами») причину его отклонения (чтобы люди могли понять, как исправить проблема)
Для простоты давайте рассмотрим одно простое десятичное целое число (введенное пользователем) и ничего больше. Возможные причины отклонения ввода пользователя:
Давайте также определим «ввод содержит недопустимые символы» правильно; и скажи что:
5» будет рассматриваться как «5»)
Из этого мы можем определить, что необходимы следующие сообщения об ошибках:
С этого момента мы можем видеть, что подходящая функция для преобразования строки в целое число должна была бы различать очень разные типы ошибок; и что то типа
scanf()
"илиatoi()
" или "strtoll()
» совершенно и совершенно бесполезно, потому что они не дают вам никаких указаний на то, что было неправильно с вводом (и используют совершенно неуместное и неуместное определение того, что является / не является действительным). вход ").Вместо этого давайте начнем писать что-то бесполезное:
Соответствовать заявленным требованиям; эта
convertStringToInteger()
функция, скорее всего, будет состоять из нескольких сотен строк кода сама по себе.Теперь, это был просто "анализ одного простого десятичного целого числа". Представьте, что вы хотите разобрать что-то сложное; как список структур "имя, улица, номер телефона, адрес электронной почты"; или, может быть, как язык программирования. В этих случаях вам может понадобиться написать тысячи строк кода, чтобы создать анализ, который не является шуткой.
Другими словами...
Напишите (потенциально тысячи строк) кода самостоятельно, чтобы удовлетворить ваши требования.
источник
Вот пример использования
flex
для сканирования простого ввода, в данном случае это файл чисел с плавающей запятой ASCII, который может быть в формате US (n,nnn.dd
) или European (n.nnn,dd
). Это просто скопировано из гораздо большей программы, поэтому могут быть некоторые неразрешенные ссылки:источник
Другие ответы дают правильные сведения низкого уровня, поэтому я ограничусь более высоким уровнем: во-первых, проанализируйте, как вы ожидаете, как будет выглядеть каждая строка ввода. Попробуйте описать ввод с помощью формального синтаксиса - если повезет, вы обнаружите, что он может быть описан с помощью обычной грамматики или, по крайней мере, безконтекстной грамматики . Если регулярной грамматики достаточно, то вы можете закодировать конечный автоматкоторый распознает и интерпретирует каждую командную строку по одному символу за раз. Ваш код будет читать строку (как объяснено в других ответах), а затем сканировать символы в буфере через конечный автомат. В определенных состояниях вы останавливаете и конвертируете отсканированную до сих пор подстроку в число или что-то еще. Вы можете, вероятно, «свернуть свое», если это так просто; если вы обнаружите, что вам нужна полная контекстно-свободная грамматика, вам лучше понять, как использовать существующие инструменты синтаксического анализа (см .:
lex
и /yacc
или их варианты).источник
errno == EOVERFLOW
после использованияstrtoll
).