Несколько слов о чтении данных с консоли и языке Си
Запись от fasked размещена 08.04.2012 в 13:21
Обновил(-а) fasked 04.07.2012 в 18:30 (Обновил и дополнил текст)
Обновил(-а) fasked 04.07.2012 в 18:30 (Обновил и дополнил текст)
Метки prompt, безопасный ввод данных, консоль, язык си
Безопасный ввод данных на Си Цель данной заметки - показать начинающим, как правильно получать данные, вводимые с консоли. Я довольно часто вижу, что студентам дают задания, в которые входит пункт о проверке ввода. Большинство способов, предлагаемых на форуме, не работают. Почему люди отдают не полностью работающее решение? Во-первых, потому что это Ваша задача. А во-вторых, потому что реализовать полностью безопасный ввод на Си - не совсем тривиальная задача. Если Вам необходимо проверять вводимые пользователем данные на корректность, то эта заметка может оказаться полезной для Вас. Также по ходу мы разберем самые распространенные ошибки, связанные с темой заметки. Что мы рассмотрим?
Когда-то я долго стучался головой об стену и клавиатуру, чтобы получить безопасный ввод данных. Теперь я хочу поделиться набитыми шишками. 1. Библиотека ввода/вывода Си "Ввод и вывод" не являются частью самого языка Си. Тем не менее стандартная библиотека libc предоставляет достаточное количество средств для взаимодействия программы с внешней средой. Функции, предоставляемые библиотекой ввода и вывода, могут быть очень простыми, например getchar и putchar, и более сложными, такими как scanf и printf, включающими в себя еще один маленький язык для обозначения типов. И все они используют так называемые потоки ввода-вывода (input/output streams). Потоки это по сути просто последовательный набор байтов. Цитата:
Не путать с многопоточностью.
Существует два англоязычных термина: и . Оба термина в русскоязычной литературе принято переводить одним словом "поток". 2. Чтение данных с консоли, используя scanf scanf это, пожалуй, самый простейший способ получить практически любые данные, которые вводит пользователь. Это очень удобная функция, особенно для быстрого написания программы, от которой не требуют качественной обработки ввода. Например, если Вы пишете маленькую утилитку для себя - используйте scanf и не парьтесь. Итак, scanf принимает форматную строку и указатели на переменные. Именно указатели. Следовательно, чтобы прочитать число, достаточно написать такой код:
Код:
Введите размер: 25 ---> Вы ввели: 25 Введите размер: hello ---> Вы ввели: 0
Логика функции scanf устроена весьма просто. Каждый символ в потоке сравнивается на соответствие с форматной строкой. Когда происходит ошибка несоответствия, scanf незамедлительно прерывается, оставляя в потоке первый несоответствующий символ. Логичным было бы попробовать убрать его из потока. Как это сделать? Нам поможет магия scanf!
Таким образом мы выбрасываем из потока все, что было в нем до того, как пользователь нажал клавишу Enter. Уменьшим счетчик, чтобы пользователь попробовал повторить ввод того же самого значения. Данный метод работает, однако, у него есть недостаток, основанный на специфике работы потоков: Цитата:
Если ввести "45abc", то scanf успешно прочтёт 45, затем на следующей итерации ввода прочтёт "abc". Что есть не совсем логично с точки зрения пользователя, потому что ошибочный ввод данных A повлечёт за собой ошибочный ввод данных B без возможности что-либо сделать. Например, сначала просят ввести число, а затем строку. Введя "45abc", пользователь введёт сразу два поля данных.
Код:
#include <stdio.h> int main() { int age = 0; char name[32] = ""; printf("Введите свой год рождения: "); if (scanf ("%d", &age) != 1) { printf("Что-то пошло не так.\n"); return 1; } printf("Введите свое имя: "); if (scanf ("%s", name) != 1) { printf("Что-то пошло не так."); return 1; } printf("Вы ввели: %s %d год", name, age); return 0; } Код:
Введите свой год рождения: 1989 блаблабла Введите свое имя: Вы ввели: блаблабла 1989 год С другой стороны мы можем отловить символ перевода строки:
Использование scanf приносит немало мучений при организации безопасного и отзывчивого интерфейса для ввода данных. Именно поэтому советуют считывать всю строку в буфер и только потом анализировать его. Такой подход применяется в уже лишь чуть-чуть серьезных приложениях. И его-то мы и обсудим далее. 3. Чтение данных с консоли, используя буфер Это наиболее широко применяемая техника для обработки ввода данных. Вместо использования форматного ввода мы просто читаем всю строку в буфер. А прочитать строку не так уж и тяжело:
Код:
BUGS Never use gets(). Because it is impossible to tell without knowing the data in advance how many characters gets() will read, and because gets() will continue to store characters past the end of the buffer, it is extremely dangerous to use. It has been used to break computer security. Use fgets() instead.
Следует отметить, что если логика программы требует получения всей строки целиком, а не кусками, то реально написать код, который бы динамически наращивал размер буфера по мере необходимости - если последний символ прочитанной строки не равен символу перевода строки.
Обычно программы не просто ждут, чтобы пользователь начал ввод данных. Как правило, они печатают какое-то приглашение на ввод, вроде как "Введите Ваше имя". Это приглашение называется prompt. Казалось бы, а что здесь может быть сложного? Дело в том, что стандартные потоки ввода/вывода буферизуются. Если Вы написали printf - это не значит, что приглашение появится именно в этот момент, когда выполнилась функция printf. Строка может остаться в буфере до лучших времен. Тем не менее программа уже будет ожидать ввода данных, а пользователь ничего об этом еще не знает. Это решается просто. Используйте функцию flush каждый раз, когда Вы хотите вывести данные на экран немедленно.
Цитата:
Если Вы раньше никогда не использовали flush и все работало вполне ожидаемо, то возможно в версии стандартной библиотеки, которую используете Вы, flush выполняется автоматически при вызове scanf. Стандартом языка такое поведение не гарантируется, следовательно и не стоит на него расчитывать.
Зачем нужна качественная обратная связь? Давайте просто посмотрим на сообщения об ошибках таких популярных инструментов как gcc и clang.
Код:
main.c: In function ‘main’: main.c:5:1: error: expected ‘;’ before ‘}’ token Код:
main.c:4:13: error: expected ';' after return statement return 0 ^ ; Представьте, если бы компилятор Вам говорил только "где-то произошла какая-то ошибка" UPDATE: В следующем примере я делаю акцент на детализацию ошибок, на идентификацию типа ошибки, на диагностику. Для такого простейшего случая я попытаюсь дать пользователю как можно больше информации об ошибке. Холивар на тему "Зачем это надо?" можно найти в комментариях. 5. Пример безопасного чтения double Итоговым примером является программа, которая читает с консоли число типа double и в случае какой-либо ошибки сообщает об этом пользователю, а также просит повторить ввод.
Примеры работы программы:
Теперь Вы можете использовать этот пример как базовый для своих будущих программ. |
Всего комментариев 28
Комментарии
-
[B]fasked[/B] единственными граблями в чтении double явлется облом программы при вводе параметра с типом double на старых компиляторах без начальной инициализации этого параметра любым стохастическим числом. Также есть один подводный камень у printf связанный с даблом - это тот факт что при печати с ключом %lf , будет печататься всего 6-ть символов после запятой (вывод дефалтится до флоат). Так вот этот момент обходим посредством спецификатора длинны формата %.11f
Лично от себя добавлю что "безопасный ввод" это огромная гора кода, которая может быть заменана лаконично простой конструкцией
[C]#include <stdio.h>
int main()
{
char bufStub = 0;
double param = 0;//Вот важный момент начальная инициализация!
while(1)
{
bufStub = 0;
printf("Enter double : ");
if(!scanf("%lf",¶m))
{
printf("Wrong input\n");
//Данный цикл отработает всегда
while(bufStub != '\n')
scanf("%c",&bufStub);
}
else
printf("Your input %.11f\n",param);//Тоже мало кто знает что %.11f позволятет
//обходить изъян связанный с дефалтным отображением дабла как float
}
return 0;
}[/C]
PS:Зачем городить какуе-то байду если можно эффективно писать в 3 строчки...
Отработку можешь посмотреть здесь [url]https://www.cyberforum.ru/blogs/34326/blog279.html[/url]Запись от -=ЮрА=- размещена 08.04.2012 в 14:16
Обновил(-а) -=ЮрА=- 08.04.2012 в 14:26 -
Цитата:Лично от себя добавлю что "безопасный ввод" это огромная гора кода, которая может быть заменана лаконично простой конструкцией
PS:Зачем городить какуе-то байду если можно эффективно писать в 3 строчки...C 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#include <stdio.h> int main() { char bufStub = 0; double param = 0;//Вот важный момент начальная инициализация! while(1) { bufStub = 0; printf("Enter double : "); if(!scanf("%lf",¶m)) { printf("Wrong input\n"); //Данный цикл отработает всегда while(bufStub != '\n') scanf("%c",&bufStub); } else printf("Your input %.11f\n",param);//Тоже мало кто знает что %.11f позволятет //обходить изъян связанный с дефалтным отображением дабла как float } return 0; }
Код:Enter double : 12f Your input 12.00000000000 Enter double : Wrong input Enter double :
Код:Enter double : 123123.123123.123 Your input 123123.12312300000 Enter double : Your input 0.12300000000 Enter double :
Код:Enter double : 123.f.12 Your input 123.00000000000 Enter double : Wrong input Enter double :
К тому же у меня есть подозрение, что мой способ очистки потока таки производительнее и как минимум лаконичнее, чем этот:
C 1 2 3
//Данный цикл отработает всегда while(bufStub != '\n') scanf("%c",&bufStub
Запись от fasked размещена 08.04.2012 в 14:24
Обновил(-а) fasked 08.04.2012 в 14:45 -
[B]fasked[/B] ну мы что маленькие, ты прекрасно понимаешь что мой код отрабатывал так потому как был отстроен на 1-у ошибку ввода. Чтож да ты прав ошибок может быть множество.
Ниже код с полной фиксацией "плохого ввода". На счёт интерактивности, ввод может быть успешным либо нет и городить гору кода чтобы написать Ввелось "не так потому что тот кто вводил редиска" или "не так потому что тот кто вводил не вышел руками" как то сомнительно.
Чтож я доработал код
[C]#include <stdio.h>
int main()
{
char bufStub = 0;
double param = 0;//Вот важный момент начальная инициализация!
while(1)
{
bufStub = 0;
printf("Enter double : ");
if(scanf("%lf%c",¶m,&bufStub) != 2)
printf("Wrong input\n");
//Данный цикл отработает всегда
if(bufStub == '\n')
printf("Your input %.11f\n",param);//Тоже мало кто знает что %.11f позволятет
//обходить изъян связанный с дефалтным отображением дабла как float
else//Очистка от неверного ввода
{
//Данный цикл отработает всегда
printf("Wrong input\n");
while(bufStub != '\n')
scanf("%c",&bufStub);
}
}
return 0;
}[/C]
Ниже отработка под твои неверные вводы
[C]Enter double : 12f
Wrong input
Enter double : 123123.123123.123
Wrong input
Enter double : 123.f.12
Wrong input
Enter double : 123.777
Your input 123.77700000000
Enter double :[/C]
В любом случае это не гора кода и "для маленьких" он гораздо понятней будет...Запись от -=ЮрА=- размещена 08.04.2012 в 16:24 -
Цитата:Код:
Enter double : aaaa Wrong input Wrong input
Цитата:
Может быть, но я ставил другие цели. Я не говорю, что я собирался написать непонятный код, а то, что я писал код, который покажет ошибку. Я также упоминал, что подход, который предлагаю я, используется в более-менее серьезных приложениях. Если прочитать заметку целиком, то можно увидеть момент, где я предлагал остановиться на достаточно простом варианте.Запись от fasked размещена 08.04.2012 в 16:31 -
Там, где "не путать с многопоточностью" можно пояснить, что в английском есть два термина. "Stream" - поток ввода-вывода. "Thread" - поток как ветвь исполнения программы. Просто эти два термина по русски переводятся одним словом
> "Проблема в том, что слово "hello" остается в потоке"
Этот момент надо объяснить поподробнее. Потому что тем, кто не знает, это совсем неочевидно. Да и до конца раздела не помешало бы чуть более подробно разжевать
> "Буфер может и не содержать символ конца строки, если пользователь ввел строку длиннее, чем LINESIZE. Тогда мы удалим символ, который важен для нас"
В этом месте надо написать, что в зависимости от логики работы программы придётся делать код, который бы динамически нарастил строку для того, чтобы можно было вместить полную введённую строку. Это понадобится в тех случаях, когда логика работы программы требует видеть всю введённую строку целиком
-------------------------------------
Юра наваял гневный ответ чемберлену: https://www.cyberforum.ru/blogs/34326/blog279.html
Как обычно это был ответ человека, который "чукча не читатель, чукча писатель", но тем не менее эту проблему заметил в том числе и я. В разделе "Пример безопасного чтения double" надо явным образом указать, почему разводится такой здоровенный код ради ввода double'а, хотя в начале статьи у тебя это делалось намного короче. Ты Юре об этом явно написал, но он этого не увидел. А это говорит о том, что причина плохо пояснена
В общем статья очень правильная и очень полезная для начинающих. Но статья написана как краткий вывод из некоей сентенции без нормального объяснения самОй сентенции. А вот на объяснение как раз-таки стоит потратить время. Потому что читающий должен чётко понимать, какие проблемы решаются и какая конкретная постановка задачи стоит. Т.е. нужно сделать хороший упор на то, что помимо безопасного ввода выполняется ещё и качественная обратная связь (детализация ошибки ввода).Запись от Evg размещена 08.04.2012 в 17:18 -
Запись от Evg размещена 08.04.2012 в 17:20 -
Цитата:В общем статья очень правильная и очень полезная для начинающих. Но статья написана как краткий вывод из некоей сентенции без нормального объяснения самОй сентенции. А вот на объяснение как раз-таки стоит потратить время. Потому что читающий должен чётко понимать, какие проблемы решаются и какая конкретная постановка задачи стоит.
Запись от fasked размещена 08.04.2012 в 17:28 -
Раздел "Чтение данных с консоли, используя scanf". Если ввести "45abc", то scanf успешно прочтёт 45, затем на следующей итерации ввода прочтёт "abc". Что есть не совсем логично с точки зрения пользователя, потому что ошибочный ввод данных A повлечёт за собой кривое вбитие данных B без возможности что-либо сделать. Например, сначала просят ввести число, а затем строку. Введя "45abc" пользователь введёт сразу два поля данных. И в этом случае для короткого примера (т.е. без детализации ошибок) логичным было воспользоваться фичей от Юры (с контролем за переводом строки)
Запись от Evg размещена 08.04.2012 в 17:48 -
[QUOTE]Enter double : aaaa
Wrong input
Wrong input[/QUOTE] - Ииии???
Вывод прекратился или что???
Enter double : dddsd
Wrong input
Wrong input
Enter double :
Да переставь условные операторы, не я это должен делать, я показал что можно обойтись без горы кода и на уровне "чукчи показал" как это сделать элегантно, остально лишь детали реализации...Запись от -=ЮрА=- размещена 08.04.2012 в 19:48 -
Запись от Evg размещена 08.04.2012 в 22:54 -
Для ознакомления
[url]https://www.cyberforum.ru/blogs/34326/blog279.html#comment1451[/url]Запись от -=ЮрА=- размещена 10.04.2012 в 20:07 -
В конце данного поста пример на тему "ради чего мы так геморроимся"
https://www.cyberforum.ru/blog... omment1474Запись от Evg размещена 11.04.2012 в 16:58 -
Запись от programina размещена 06.06.2012 в 20:04 -
> А во-вторых, потому что реализовать полностью безопасный ввод на Си не совсем тривиальная задача
Там тире должно быть
> И все они используют так называемые потоки
Думаю, лучше написать как "И все они используют так называемые потоки ввода-вывода (input-output stream)"
> Существует два англоязычных термина: thread и stream
Редиректор формуа косячит, а потому хз куда эти ссылки ведут
> Логика функции scanf устроена весьма просто
Если это новый абзац, то надо перевод строки воткнуть.
> Когда происходит ошибка несоотвествия
несоответствия
> Если ввести "45abc", то scanf успешно прочтёт 45
Это мой собственный текст, но я с трудом его осилил. Думаю, полезно было бы продемонстрировать на конкретном примере
> Такой подход применяется в уже лишь чуть-чуть серьезных приложениях
Шо такое "в уже лишь чуть-чуть"
> Но она, в отличие от gets, помещает в буфер еще и символ перевода строки
И в обязательном порядке завершает строку символом \0
> Следует отметить, что если логика программы требует получения всей строки целиком
Опять абзац
> Пример безопасного чтения double
Полезно было бы отобразить пример работы программыЗапись от Evg размещена 06.06.2012 в 20:43 -
Запись от fasked размещена 21.06.2012 в 19:11 -
https://www.cyberforum.ru/c-be... 75050.html
У человека проблема - ему нужно считать один символ. И такая постановка довольно-таки стандартная, в том смысле, что я часто видел, как люди об это спотыкаются. Неплохо бы добавить пример для решения этой проблемы.
И неплохо бы пронумеровать разделы, чтобы можно было людей отсылать на статью со словами типа "прочти раздел N3"Запись от Evg размещена 22.06.2012 в 13:51 -
Цитата:https://www.cyberforum.ru/c-be... 75050.html
У человека проблема - ему нужно считать один символ. И такая постановка довольно-таки стандартная, в том смысле, что я часто видел, как люди об это спотыкаются. Неплохо бы добавить пример для решения этой проблемы.
Номера добавил.Запись от fasked размещена 04.07.2012 в 18:38 -
Смысла нет для того, кто всё знает. Для тех, кто прочёл статью с условно нулевым уровнем входа - смысл есть. Спроецировать одно на другое для начинающего не всегда будет тривиально. Хотя я бы сделал немного наоборот. В качестве первого пояснения добавил бы пояснение на примере ввода yes/no, а пример ввода double'а, как более сложный, был логическим продолжением. Но. наверное, и вправду незачем сейчас что-либо перелопачивать
Запись от Evg размещена 04.07.2012 в 19:25 -
[url]https://www.cyberforum.ru/c-beginners/thread630861.html#post3318854[/url]
Запись от alkagolik размещена 01.08.2012 в 20:22 -
Вопрос по логике построения кода
fasked, а почему ты выбрал в функции безопасного ввода double именно
? Почему не проверку на NULL и передачу управления вызывающей функции с выводом сообщения об ошибке? Например, программа могла выделять динамическую память, и перед выходом из программы её стоит освобождать, или не нужно?C 1 2 3
/* Проверка параметров */ assert(value); assert(prompt);
Не по теме:
Пост полезный, очень детальный подход к проблеме.
Запись от #pragma размещена 06.08.2012 в 23:33