Урок №72. Обработка некорректного пользовательского ввода

  Юрий  | 

  Обновл. 1 мая 2019  | 

 25097

 ǀ   14 

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

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

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

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

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

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

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

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

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

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

   Оператор >> извлекает столько данных из входного буфера в переменную, сколько позволяет размер самой переменной (игнорируя любые пробелы, табы или \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. Хотя мы можем извлечь символ *, но в буфере останется лишний балласт, который в будущем может привести к проблемам.

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

Мы выделили 4 типа:

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

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

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

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

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

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

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


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

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

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

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

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

   Если совпадает, то значения благополучно возвращаются.

   Если нет, то сообщаем пользователю что что-то пошло не так, и просим повторить ввод снова.

Вот обновлённая функция 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 (так как пользователь в конце ввода нажимает Enter), то мы можем сообщить 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. Извлечение выполняется успешно, но пользователь ввёл слишком большое значение

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

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

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

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

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


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

Заключение

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

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

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

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

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

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

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

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

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

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (133 оценок, среднее: 4,90 из 5)
Загрузка...

Комментариев: 14

  1. Аватар vik:

    "std::int16_t x { 0 }; // переменная x занимает 16 бит, её диапазон значений: от -32 768 до 32 767"

    "Следовательно, x останется с инициализированным значением 32767"

    Почему 32767, если переменная инициализирована значением 0?

  2. Аватар Алексей:

    В случаи один лучше использовать RegEX, но это другая история)

  3. Аватар zvezdonom:

    Вот переделал функцию getValue().
    Теперь если ввести что-то такое "4657лапивпо", то уже не
    прокатит. Это ответ Alexey-ю. Так же не прокатит такой ввод "влпр 474".

    1. Аватар Andrey:

      Пропущена библиотека #include <cstdlib>, не забудьте подключить

  4. Аватар Alex:

    В конце статьи конечная версия кода для проверки вводимого значения, как я понял. Но я нашел уязвимое место. Да, он подходит, если мы вводим только буквы (gsrjtykj), буквы-цифры (fawy426), но если ввести цифры-буквы (15gdat), то код становится бесполезным.

    1. Аватар Molex:

      Ну так предполагается, что ты будешь сбрасывать балласт из букв.

  5. Аватар Alexey:

    Я вот не понял, если мне надо ввести число int, пользователь ввел, например 4545врвр, то используя if (std::cin.fail()) компилятор присвоит 4545 переменной, а "врвр" отбросит. А как ему сообщит что это ввод полностью неверный?

  6. Аватар Алексей:

    Подскажите пожалуйста, а как программа определяет что предыдущий ввод провальный?

    1. Евгений Евгений:

      cin.fail() даст true если ввод был некорректный т.е cin вошел в режим отказа.

  7. Аватар Герман:

    Понял. Спасибо!!!

    1. Юрий Юрий:

      Пожалуйста 🙂

  8. Аватар Герман:

    Уважаемый автор, поясните, пожалуйста, как появляется цифра 32767 в коде (в двоичной системе исчисления это 15 единиц), переменная double — 8 байтов, это 1.844674407371E+19.

    1. Юрий Юрий:

      cin.ignore — это стандартное решение из библиотеки iostream. Число, которое указывается в скобках — выборочное, можно указать 1, можно 100, можно 270, а можно 32767. Это число не является результатом каких-либо вычислений, вы указываете то, что хотите сами — сколько символов вы считаете нужным убрать из входного буфера.

      1. Аватар Юлия:

        32767 — это максимум, который помещается в int. Т.е. для int игнорировать 32767 то же самое, что игнорировать "всё". Правильно?

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

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

telegram канал
НОВОСТИ RAVESLI