Большинство программ, имеющих хоть какой-либо пользовательский интерфейс, сталкиваются с обработкой пользовательского ввода. В программах, которые мы писали раньше, используется std::cin для получения пользовательского ввода. А так как ввод данных имеет свободную форму (пользователь может ввести всё, что пожелает), то пользователю очень легко ввести данные, которые от него никак не ожидаются.
При написании программ, вы всегда должны учитывать, как пользователи (преднамеренно или непреднамеренно) могут использовать ваши программы. Хорошо написанная программа предвидит, как ею могут злоупотребить, и либо будет обрабатывать такие случаи, либо предотвращать их (если это возможно). Программа, которая имеет обработку некорректного ввода, называется надежной.
На этом уроке мы рассмотрим, как пользователи могут вводить некорректные данные через std::cin, а также как с этим бороться.
std::cin, буфер данных и извлечение
Прежде чем разбираться с обработкой некорректного ввода через std::cin и оператор >>
, давайте сначала рассмотрим их принцип работы.
Процесс, когда мы используем оператор >>
для получения данных от пользователя и размещение этих данных в определенной переменной, называется извлечением. Соответственно, оператор >>
называется оператором извлечения.
Когда пользователь вводит данные в ответ на операцию извлечения, то эти данные помещаются в буфер std::cin. Буфер данных — это просто часть памяти, зарезервированная для временного хранения данных, когда они перемещаются из одного места в другое. В этом случае буфер используется для хранения пользовательского ввода, пока он находится в режиме ожидания выделения для него переменных.
При использовании оператора извлечения, выполняется следующая процедура:
Если во входном буфере есть данные, то эти данные используются для извлечения.
Если во входном буфере нет данных, то пользователю предлагается ввести данные (обычно именно это и происходит в большинстве случаев). Когда пользователь нажимает Enter, символ новой строки \n
помещается во входной буфер.
Оператор >>
извлекает столько данных из входного буфера в переменную, сколько позволяет размер самой переменной (игнорируя любые пробелы, табы или \n
).
Любые данные, которые не были извлечены, остаются во входном буфере для последующего извлечения.
Извлечение данных считается успешным, если по крайней мере один символ был извлечен из входного буфера. Оставшиеся данные во входном буфере используются для последующих извлечений. Например:
1 2 |
int a; std::cin >> a; |
Если пользователь введет 7d
, то 7
будет извлечено, преобразовано в целое число и присвоено переменной a
. А d\n
останется во входном буфере дожидаться следующего извлечения.
Извлечение не выполняется, если данные ввода не соответствуют типу переменной, выделенной для них. Например:
1 2 |
int a; std::cin >> a; |
Если бы пользователь ввел z
, то извлечение не выполнилось бы, так как z
не может быть извлечено в целочисленную переменную.
Проверка пользовательского ввода
Существует три основных способа проверки пользовательского ввода:
До ввода
Предотвращение некорректного пользовательского ввода.
После ввода
Пользователь может вводить всё, что хочет, но осуществляется последующая проверка данных. Если проверка прошла успешно, то выполняется перемещение данных в переменную.
Пользователь может вводить всё, что хочет, но при операции извлечения данных оператором >>
параллельно решаются возможные ошибки.
Некоторые графические пользовательские интерфейсы или расширенные текстовые интерфейсы позволяют проверять ввод пользователя сразу (символ за символом). Программист создает функцию проверки данных, которая принимает и проверяет пользовательский ввод, и, если данные ввода корректны, то возвращается true, если нет — false. Эта функция вызывается каждый раз, когда пользователь нажимает на клавишу. Если функция проверки возвращает true, то символ, который пользователь ввел — принимается. Если функция возвращает false, то символ, который только что ввел пользователь — отбрасывается (и не отображается на экране). Используя этот метод, мы можем гарантировать, что любой пользовательский ввод будет корректным, так как любые неверные нажатия клавиш будут обнаружены и немедленно удалены. К сожалению, std::cin не поддерживает этот тип проверки.
Поскольку строки не имеют никаких ограничений на то, какие символы вводятся, то извлечение гарантированно будет успешным (хотя помним, что std::cin останавливает процесс извлечения при первом обнаружении символа пробела). После ввода строки программа сразу может её проанализировать. Однако этот анализ и последующая конвертация данных в другие типы данных (например, числа) может быть сложной, поэтому это делается только в редких случаях.
Чаще всего мы позволяем std::cin и оператору извлечения выполнять тяжелую работу. В соответствии с этим методом пользователь может вводить всё, что хочет, а далее std::cin и оператор >>
пытаются извлечь данные и, если что-то пойдет не так, выполняется обработка возможных ошибок. Это самый простой способ, и его мы и будем рассматривать.
Рассмотрим следующую программу «Калькулятор», которая не имеет обработки ошибок:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <iostream> double getValue() { std::cout << "Enter a double value: "; double a; std::cin >> a; return a; } char getOperator() { std::cout << "Enter one of the following: +, -, *, or /: "; char sm; std::cin >> sm; return sm; } void printResult(double a, char sm, double b) { if (sm == '+') std::cout << a << " + " << b << " is " << a + b << '\n'; else if (sm == '-') std::cout << a << " - " << b << " is " << a - b << '\n'; else if (sm == '*') std::cout << a << " * " << b << " is " << a * b << '\n'; else if (sm == '/') std::cout << a << " / " << b << " is " << a / b << '\n'; } int main() { double a = getValue(); char sm = getOperator(); double b = getValue(); printResult(a, sm, b); return 0; } |
Здесь мы просим пользователя ввести два числа и арифметический оператор. Результат выполнения программы:
Enter a double value: 4
Enter one of the following: +, -, *, or /: *
Enter a double value: 5
4 * 5 is 20
Теперь рассмотрим, где некорректный ввод пользователя может привести к сбою в программе.
Сначала мы просим пользователя ввести первое число. А что будет, если он введет что-либо другое (например, q
)? В этом случае извлечение данных не произойдет.
Во-вторых, мы просим пользователя ввести один из 4 возможных символов (арифметических операторов). Что будет, если он введет какой-то другой символ? Мы сможем извлечь данные, но не сможем их обработать.
В-третьих, что будет, если пользователь вместо символа введет строку, например, *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
Здесь мы просим пользователя ввести один из 4 арифметических операторов, но он вводит k
. Символ k
является допустимым символом, поэтому std::cin извлекает его в переменную sm
, и всё это добро возвращается обратно в функцию main(). Но в программе не предусмотрен случай обработки подобного ввода, поэтому мы получаем сбой, и в результате ничего не выводится.
Решение простое: выполнить проверку пользовательского ввода. Обычно она состоит из трех шагов:
Проверить пользовательский ввод на ожидаемые значения.
Если ввод совпадает с ожидаемым, то значения благополучно возвращаются.
Если нет, то сообщаем пользователю что что-то пошло не так, и просим повторить ввод снова.
Вот обновленная функция getOperator() с проверкой пользовательского ввода:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
char getOperator() { while (true) // цикл продолжается до тех пор, пока пользователь не введет корректное значение { std::cout << "Enter one of the following: +, -, *, or /: "; char sm; std::cin >> sm; // Выполняем проверку значений if (sm == '+' || sm == '-' || sm == '*' || sm == '/') return sm; // возвращаем данные в функцию main() else // в противном случае, сообщаем пользователю, что что-то пошло не так std::cout << "Oops, that input is invalid. Please try again.\n"; } } |
Мы используем цикл 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 (вставляя символ новой строки), то всё происходит на одной строке.
Хотя результат программы правильный, но её выполнение — нет. Согласитесь, что с наличием возможности просто проигнорировать лишние символы было бы намного лучше. К счастью, это можно сделать следующим образом:
1 |
std::cin.ignore(32767, '\n'); // удаляем до 32767 символов из входного буфера вплоть до появления символа '\n' (который мы также удаляем) |
Поскольку последний символ, введенный пользователем, должен быть \n
(так как пользователь в конце ввода нажимает Enter), то мы можем сообщить std::cin игнорировать все символы в буфере до тех пор, пока не будет найден символ новой строки (который мы также удаляем). Таким образом, всё лишнее будет успешно проигнорировано.
Обновим нашу функцию getDouble(), добавив эту строку:
1 2 3 4 5 6 7 8 |
double getValue() { std::cout << "Enter a double value: "; double a; std::cin >> a; std::cin.ignore(32767, '\n'); // удаляем до 32767 символов из входного буфера вплоть до появления символа '\n' (который мы также удаляем) return a; } |
Теперь наша программа будет работать так, как надо, даже если мы введем 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 переходит в «режим отказа». Как только установлен этот режим, то все последующие запросы на извлечение данных будут проигнорированы.
К счастью, мы можем определить, удачно ли прошло извлечение или нет. Если нет, то мы можем исправить ситуацию следующим образом:
1 2 3 4 5 |
if (std::cin.fail()) // если предыдущее извлечение было неудачным, { std::cin.clear(); // то возвращаем cin в 'обычный' режим работы std::cin.ignore(32767,'\n'); // и удаляем значения предыдущего ввода из входного буфера } |
Вот так! Теперь давайте интегрируем это в нашу функцию getValue():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
double getValue() { while (true) // цикл продолжается до тех пор, пока пользователь не введет корректное значение { std::cout << "Enter a double value: "; double a; std::cin >> a; if (std::cin.fail()) // если предыдущее извлечение оказалось неудачным, { std::cin.clear(); // то возвращаем cin в 'обычный' режим работы std::cin.ignore(32767,'\n'); // и удаляем значения предыдущего ввода из входного буфера } else // если всё хорошо, то возвращаем a return a; } } |
Ошибка №4: Извлечение выполняется успешно, но пользователь ввел слишком большое числовое значение
Рассмотрим следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> #include <cstdint> int main() { std::int16_t x { 0 }; // переменная x занимает 16 бит, её диапазон значений: от -32768 до 32767 std::cout << "Enter a number between -32768 and 32767: "; std::cin >> x; std::int16_t y { 0 }; // переменная y занимает 16 бит, её диапазон значений: от -32768 до 32767 std::cout << "Enter another number between -32768 and 32767: "; std::cin >> y; std::cout << "The sum is: " << x + y << '\n'; return 0; } |
Что случится, если пользователь введет слишком большое число (например, 40000
)?
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).
Объединяем всё вместе
Вот программа «Калькулятор», но уже с полным механизмом обработки/проверки ошибок:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
#include <iostream> double getValue() { while (true) // цикл продолжается до тех пор, пока пользователь не введет корректное значение { std::cout << "Enter a double value: "; double a; std::cin >> a; // Проверка на предыдущее извлечение if (std::cin.fail()) // если предыдущее извлечение оказалось неудачным, { std::cin.clear(); // то возвращаем cin в 'обычный' режим работы std::cin.ignore(32767,'\n'); // и удаляем значения предыдущего ввода из входного буфера std::cout << "Oops, that input is invalid. Please try again.\n"; } else { std::cin.ignore(32767,'\n'); // удаляем лишние значения return a; } } } char getOperator() { while (true) // цикл продолжается до тех пор, пока пользователь не введет корректное значение { std::cout << "Enter one of the following: +, -, *, or /: "; char sm; std::cin >> sm; // Переменные типа char могут принимать любые символы из пользовательского ввода, поэтому нам не стоит беспокоиться по поводу возникновения неудачного извлечения std::cin.ignore(32767,'\n'); // удаляем лишний балласт // Выполняем проверку пользовательского ввода if (sm == '+' || sm == '-' || sm == '*' || sm == '/') return sm; // возвращаем обратно в caller else // в противном случае, сообщаем пользователю что что-то пошло не так std::cout << "Oops, that input is invalid. Please try again.\n"; } } void printResult(double a, char sm, double b) { if (sm == '+') std::cout << a << " + " << b << " is " << a + b << '\n'; else if (sm == '-') std::cout << a << " - " << b << " is " << a - b << '\n'; else if (sm == '*') std::cout << a << " * " << b << " is " << a * b << '\n'; else if (sm == '/') std::cout << a << " / " << b << " is " << a / b << '\n'; else std::cout << "Something went wrong: printResult() got an invalid operator."; } int main() { double a = getValue(); char sm = getOperator(); double b = getValue(); printResult(a, sm, b); return 0; } |
Заключение
При написании программ, всегда думайте о том, как пользователи могут злоупотребить ими или использовать не по назначению, особенно то, что касается ввода данных. Подумайте:
Может ли извлечение не выполниться?
Может ли пользователь ввести значение больше ожидаемого?
Может ли пользователь ввести бессмысленные значения?
Может ли ввод пользователя привести к переполнению переменных?
Используйте ветвление if и логические переменные для проверки пользовательского ввода.
Следующий код осуществляет проверку пользовательского ввода и исправляет проблемы с переполнением или неудачным извлечением данных:
1 2 3 4 5 |
if (std::cin.fail()) // если предыдущее извлечение не выполнилось или произошло переполнение, { std::cin.clear(); // то возвращаем cin в 'обычный' режим работы std::cin.ignore(32767,'\n'); // и удаляем значения предыдущего ввода из входного буфера } |
Примечание: Проверка пользовательского ввода очень важна и полезна, но она, к сожалению, приводит к усложнению кода, что, в свою очередь, затрудняет его понимание. Поэтому на следующих уроках мы не будем её использовать, дабы всё оставалось максимально простым и доступным.
"std::int16_t x { 0 }; // переменная x занимает 16 бит, её диапазон значений: от -32 768 до 32 767"
…
"Следовательно, x останется с инициализированным значением 32767"
Почему 32767, если переменная инициализирована значением 0?
Наверно, вот об этом "Следовательно, переменной x присваивается максимально возможное значение типа данных в этом случае, 32767"
Тоже бы послушал почему не 0.
В случаи один лучше использовать RegEX, но это другая история)
А как использовать регулярные выражения в C++?
Вот переделал функцию getValue().
Теперь если ввести что-то такое "4657лапивпо", то уже не
прокатит. Это ответ Alexey-ю. Так же не прокатит такой ввод "влпр 474".
Пропущена библиотека #include <cstdlib>, не забудьте подключить
В конце статьи конечная версия кода для проверки вводимого значения, как я понял. Но я нашел уязвимое место. Да, он подходит, если мы вводим только буквы (gsrjtykj), буквы-цифры (fawy426), но если ввести цифры-буквы (15gdat), то код становится бесполезным.
Ну так предполагается, что ты будешь сбрасывать балласт из букв.
Я вот не понял, если мне надо ввести число int, пользователь ввел, например 4545врвр, то используя if (std::cin.fail()) компилятор присвоит 4545 переменной, а "врвр" отбросит. А как ему сообщит что это ввод полностью неверный?
Можно добавить в это условие дополнительную проверку — std::cin.peek() != '\n'
Хороший совет! Мне помогло мгновенно и на выходе получилось следующее:
теперь программа не сохраняет ввод 66qbst как 66, а просит ввести нормально
Подскажите пожалуйста, а как программа определяет что предыдущий ввод провальный?
cin.fail() даст true если ввод был некорректный т.е cin вошел в режим отказа.
Понял. Спасибо!!!
Пожалуйста 🙂
Уважаемый автор, поясните, пожалуйста, как появляется цифра 32767 в коде (в двоичной системе исчисления это 15 единиц), переменная double — 8 байтов, это 1.844674407371E+19.
cin.ignore — это стандартное решение из библиотеки iostream. Число, которое указывается в скобках — выборочное, можно указать 1, можно 100, можно 270, а можно 32767. Это число не является результатом каких-либо вычислений, вы указываете то, что хотите сами — сколько символов вы считаете нужным убрать из входного буфера.
32767 — это максимум, который помещается в int. Т.е. для int игнорировать 32767 то же самое, что игнорировать "всё". Правильно?