Урок 72. Обработка некорректного ввода через std::cin

   ⁄ 

 Обновлено 29 Июл 2017

  ⁄   

Большинство программ, имеющих хоть какой-либо пользовательский интерфейс, сталкиваются с обработкой ввода данных. В программах, которые мы писали раньше, используется std::cin для получения ввода от пользователя. А так как ввод текста имеет свободную форму (пользователь может ввести всё, что пожелает), то пользователю очень легко ввести значение, которое от него никак не ожидается.

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

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

std::cin, буфер памяти и извлечение данных

Прежде чем разбираться с обработкой некорректного ввода через std::cin и оператор >>, сначала рассмотрим их принцип работы.

Процесс, когда мы используем оператор >>, чтобы получить данные от пользователя и поместить их в определенную переменную, называется извлечением. Соответственно в этом случае, оператор >> называется оператором извлечения.

Когда пользователь вводит данные в ответ на операцию извлечения, то эти данные помещаются в буфер std::cin. Буфер памяти (или буфер данных) — это просто часть памяти, зарезервированная для временного хранения данных, когда они перемещаются из одного места в другое. В этом случае буфер используется для хранения пользовательского ввода, пока он находится в режиме ожидании выделения для него переменных.

При использовании оператора извлечения, выполняется следующая процедура:

если во входном буфере есть данные, то эти данные используются для извлечения;

если во входном буфере нет данных, то пользователю предлагается ввести данные для извлечения (обычно именно это и происходит в большинстве случаев). Когда пользователь нажимает «Enter», символ «\n» помещается во входной буфер;

оператор >> извлекает столько данных из входного буфера в переменную, сколько позволяет размер самой переменной (игнорируя любые символы whitespace: пробелы, табы или «\n»);

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

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

Если пользователь введет «7d», 7 будет извлечено, преобразовано в целое число и присвоено переменной a. А «d\n» останется во входном буфере дожидаться следующего извлечения.

Извлечение не выполняется, если данные ввода не соответствуют типу переменной, выделенной для них. Например:

Если бы пользователь ввел «z», то извлечение не выполнилось бы, так как «z» не может быть извлечено в целочисленную переменную.

Проверка пользовательского ввода данных

Процесс проверки пользовательского ввода на соответствие с тем, что от него ожидается, называется проверкой ввода.

Существует три основных способа:

  До ввода

Предотвращение некорректного пользовательского ввода.

  После ввода

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

Пользователь может вводить всё, что хочет, но при операции извлечения данных оператором >> параллельно создаются решения возможных ошибок.

Некоторые графические пользовательские интерфейсы или расширенные текстовые интерфейсы позволяют проверять ввод пользователя сразу, как только он его вводит (символ за символом). Программист создает функцию проверки данных, которая принимает и проверяет пользовательский ввод, и если введенные данные корректны, то возвращается true, если нет, то false. Эта функция вызывается каждый раз, когда пользователь нажимает на клавишу. Если функция проверки возвращает true, клавиша с символом, на которую только что нажал пользователь, принимается. Если функция возвращает false, то символ, который только что ввел пользователь, отбрасывается (и не отображается на экране). Используя этот метод, мы можем гарантировать, что любой пользовательский ввод будет корректным, так как любые неверные нажатия клавиш будут обнаружены и немедленно удалены. К сожалению, std::cin не поддерживает этот тип проверки.

Поскольку строки не имеют никаких ограничений на то, какие символы вводятся, то извлечение гарантировано будет успешным (хотя помним, что std::cin останавливает процесс извлечения при первом символе пробела). После ввода строки программа сразу может проанализировать её на корректность. Однако этот анализ и последующая конвертация данных в другие типы (например, числа) может быть сложной, поэтому это делается только в редких случаях.

Чаще всего мы позволяем std::cin и оператору извлечения выполнять тяжелую работу. В соответствии с этим методом пользователь может вводить всё, что хочет, далее std::cin и оператор >> пытаются извлечь данные и если что-то пойдет не так, то выполняются кейсы обработки возможных ошибок. Это самый простой способ, и его мы и будем рассматривать.

Пример программы

Рассмотрим следующую программу «Калькулятор», которая не имеет кейсов обработки ошибок:

Здесь мы просим пользователя ввести два числа и арифметический оператор.

Enter a double value: 4
Enter one of the following: +, -, *, or /: *
Enter a double value: 5
4 * 5 is 20

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

Сначала мы просим пользователя ввести первое число. А что будет, если он введет что-то другое (например, «q»)? В этом случае извлечение не выполниться.

Во-вторых, мы просим пользователя ввести один из четырех возможных символов (арифметических операторов). Что будет, если он введет какой-то другой символ? Мы сможем извлечь данные, но не сможем их обработать.

В-третьих, что будет, если пользователь вместо символа введет строку, например, «*q hello». Хотя мы можем извлечь символ «*», но в буфере останется лишний балласт, который в будущем может привести к проблемам.

Основные типы некорректного ввода

Мы выделили четыре типа:

1. Извлечение выполняется успешно, но значения бесполезны для программы (например, вместо математического оператора введен символ «k»).

2. Извлечение выполняется успешно, но пользователь вводит лишний текст (например, «*q hello» вместо одного символа математического оператора).

3. Извлечение не выполняется (например, вместо числового значения ввели символ «q»).

4. Извлечение выполняется успешно, но пользователь ввел слишком большое число.

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

Давайте рассмотрим каждую из ситуаций выше и то, как с ними справиться.

Ошибка №1: Извлечение выполняется успешно, но данные бесполезны

Это самый простой случай. Рассмотрим следующий фрагмент кода:

Enter a double value: 4
Enter one of the following: +, -, *, or /: k
Enter a double value: 5

Здесь мы просим пользователя ввести один из четырех арифметических операторов, но он ввел «k». «k» является допустимым символом, std::cin извлекает его в переменную sm, и это всё добро возвращается обратно в функцию main(). Но в программе не предусмотрен случай с подобным вводом, и поэтому мы получаем сбой и в результате ничего не выводится.

Решение простое: выполнить проверку ввода. Обычно она состоит из трех шагов:

  1. Проверить ввод пользователя на ожидаемые значения.
  2. Если совпадает, то значения благополучно возвращаются.
  3. Если нет, то сообщаем пользователю что что-то пошло не так, и просим попробовать снова.

Вот обновленная функция getOperator() с проверкой ввода.

Мы используем цикл while для гарантирования корректного ввода. Если пользователь введет один из ожидаемых символов, то всё хорошо — символ возвратиться в функцию main(), если нет, то пользователю выведется просьба попробовать снова. И пробовать снова он будет до тех пор, пока не введет корректное значение, не закроет программу или не уничтожит компьютер 🙂

Ошибка №2: Извлечение выполняется успешно, но пользователь вводит лишний текст

Рассмотрим следующее выполнение программы «Калькулятор»:

Enter a double value: 4*5

Как вы думаете, что произойдет дальше?

Enter a double value: 4*5
Enter one of the following: +, -, *, or /: Enter a double value: 4 * 5 is 20

Программа выводит верный результат, но её порядок выполнения неправильный. Почему так? Рассмотрим ход выполнения программы детальнее.

Когда пользователь вводит «4*5», то эти данные поступают в буфер. Затем оператор >> извлекает 4 в переменную a, оставляя «*5\n» в буфере. Затем программа выводит «Enter one of the following: +, -, *, or /:». Однако, когда вызывается оператор извлечения, он видит, что в буфере находятся «*5\n», поэтому он использует эти данные, вместо того, чтобы запрашивать их у пользователя еще раз. Следовательно, извлекается символ «*», а «5\n» остаются в буфере.

После того, как пользователя просят ввести другое число, «5» извлекается из буфера, не дожидаясь ответа от пользователя. Поскольку у пользователя не было возможности ввести другие значения и нажать Enter (вставляя символ новой строки), то всё происходит на одной строке.

Хотя результат программы правильный, но её выполнение нет. Было бы намного лучше, если бы лишние символы можно было бы просто проигнорировать. К счастью, это сделать осуществить:

Поскольку последний символ, введенный пользователем, должен быть «\n», то мы можем сообщить std::cin игнорировать все символы в буфере до тех пор, пока не будет найден символ новой строки (который мы также удалили). Таким образом, всё что лишнее — успешно будет проигнорировано.

Обновим нашу функцию getDouble(), добавив эту фичу:

Теперь наша программа будет работать так, как надо, даже если мы введем «4*5» в первой строке. Число 4 будет извлечено в переменную, а все остальные символы будут удалены из входного буфера. Поскольку входной буфер теперь пуст, то при следующем выполнении операции извлечения всё пройдет гладко и порядок выполнения программы не будет нарушен.

Ошибка №3: Извлечение не выполняется

Рассмотрим следующее выполнение программы «Калькулятор»:

Enter a double value: a

Теперь уже не удивительно, что программа работает не так, как надо, но её дальнейший ход выполнения – вот что интересно:

Enter a double value: a
Enter one of the following: +, -, *, or /: Enter a double value:

И программа внезапно обрывается.

Этот случай похож на ошибку №2, но все же несколько отличается. Давайте рассмотрим детальнее, что здесь происходит.

Когда пользователь вводит «a», то этот символ помещается в буфер. Затем оператор >> пытается извлечь «a» в переменную a типа double. Поскольку «a» нельзя конвертировать в double, то оператор >> не может выполнить извлечение. На этом этапе случаются две вещи: «a» остается в буфере, а std::cin переходит в «режим отказа». Как только установлен этот режим, то все следующие запросы на извлечение данных будут игнорироваться.

К счастью, мы можем определить, удачное ли извлечение и если нет, то исправить ситуацию:

Вот так!

Давайте интегрируем это в нашу функцию getValue():

Ошибка №4. Извлечение выполняется успешно, но пользователь ввел слишком большое число

Рассмотрим следующий код:

Что случится, если пользователь введет слишком большое число (например, 40000)?

Enter a number between -32768 and 32767: 40000
Enter another number between -32768 and 32767: The sum is: 0

В примере выше std::cin немедленно перейдет в «режим отказа», и значение не будет присвоено переменной x. Следовательно, x остается с инициализированным значением 0. Все следующие вводы игнорируются, и y остается также с инициализированным значением 0. Решение такое же, как и в случае с неудачным извлечением (ошибка №3).

Объединяем всё вместе

Вот программа «Калькулятор», но уже с полным механизмом проверки ошибок:

Выводы

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

Может ли извлечение не выполниться?

Может ли пользователь ввести значения больше ожидаемого?

Может ли пользователь ввести бессмысленные значения?

Может ли ввод пользователя привести к переполнению переменных?

Используйте операторы if и логические переменные для проверки ввода.

Следующий код осуществляет проверку ввода и исправляет проблемы с переполнением или неудачным извлечением:

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

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (6 оценок, среднее: 4,50 из 5)
Загрузка...
Поделиться в:
Подписаться на обновления:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *