Урок №75. Обробка некоректного користувацького вводу

  Юрій  | 

  Оновл. 23 Чер 2020  | 

 178

Більшість програм, які мають хоч якийсь користувацький інтерфейс, стикаються з обробкою користувацького вводу. У програмах, які ми писали раніше, ми використовували 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)? В цьому випадку вилучення даних не відбудеться.

По-друге, ми просимо користувача ввести один з 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(). Але в програмі не передбачено випадок обробки подібного вводу, тому ми отримуємо збій і в результаті нічого не виводиться на екран.

Рішення просте — виконати перевірку користувацького вводу. Зазвичай вона складається з 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 (так як користувач в кінці вводу натискає 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: Вилучення виконується успішно, але користувач ввів занадто велике значення

Розглянемо наступний код:

Що станеться, якщо користувач введе занадто велике значення (наприклад, 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).

Об’єднуємо все разом

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

Висновки

При написанні програм, завжди думайте про те, як користувачі можуть зловживати програмами або використовувати не за призначенням (особливо те, що стосується вводу даних). Подумайте:

   Чи може вилучення не відбутися?

   Чи може користувач ввести більше очікуваного значення?

   Чи може користувач ввести повну маячню?

   Чи можуть дані користувача призвести переповнення змінних?

Використовуйте розгалуження if/else і змінні логічного типу даних для перевірки користувацького вводу.

Наступний код здійснює перевірку користувацького вводу і виправляє проблеми з переповненням чи невдалим вилученням даних:

Примітка: Перевірка користувацького вводу дуже важлива і корисна, але вона, на жаль, призводить до ускладнення розуміння коду. Тому в наступних уроках ми не будемо її писати, щоб все залишалося максимально простим і доступним.

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (1 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

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