На цьому уроці ми розглянемо стани потоку, валідацію користувацького вводу і якою вона буває, а також нюанси, пов’язані з цією темою в мові С++.
Стани потоку
Клас ios_base містить наступні флаги для позначення стану потоків:
goodbit — все добре;
badbit — відбулася якась фатальна помилка (наприклад, програма спробувала прочитати дані після кінця файлу);
eofbit — потік досяг кінця файлу;
failbit — відбулася якась НЕ фатальна помилка (наприклад, користувач ввів букви, коли програма очікувала числа).
Хоча ці флаги знаходяться в ios_base, але оскільки ios є дочірнім класом для ios_base, доступ до цих флагів також можливий і через ios (наприклад, як std::ios::failbit).
ios також надає список методів для доступу до вищеперерахованих станів потоку:
good() — повертає true, якщо встановлений goodbit (значить, що з потоком все ок);
bad() — повертає true, якщо встановлений badbit (значить, що відбулася якась фатальна помилка);
eof() — повертає true, якщо встановлений eofbit (значить, що потік знаходиться в кінці файлу);
fail() — повертає true, якщо встановлений failbit (значить, що відбулася якась НЕ фатальна помилка);
clear() — скидає всі поточні флаги стану потоку і задає йому goodbit;
clear(state) — скидає всі поточні флаги стану потоку і встановлює флаг, переданий в якості параметра (state);
rdstate() — повертає поточні встановлені флаги;
setstate(state) — встановлює флаг стану, переданий в якості параметра (state).
Найчастіше ми матимемо справу з failbit, який спрацьовує при некоректному користувацькому вводі. Наприклад:
|
1 2 3 4 5 6 7 8 |
#include <iostream> int main() { std::cout << "Enter your age: "; int nAge; std::cin >> nAge; } |
Зверніть увагу, ця програма очікує від користувача ввід цілого числа. Однак, якщо користувач введе що-небудь інше (наприклад, Tom), то cin не зможе помістити це в nAge, і для потоку буде встановлено флаг failbit.
Якщо ж виникає помилка, і для потоку заданий будь-який інший флаг (відмінний від goodbit), то подальші операції з цим потоком будуть проігноровані. Це можна виправити, викликавши метод clear().
Валідація користувацького вводу
Валідація користувацького вводу — це процес перевірки того, чи відповідає користувацький ввід заданим критеріям. Зазвичай, валідація вводу буває числовою і рядковою.
З рядковою валідацією ми приймаємо весь користувацький ввід в якості рядка, а потім або приймаємо цей рядок, або відхиляємо його (в залежності від критеріїв перевірки). Наприклад, якщо ми просимо користувача ввести номер телефону, то ми повинні переконатися, що цей номер складається з 10 цифр. У більшості мов програмування (особливо в скриптових, таких як Perl і PHP) це можна зробити за допомогою регулярних виразів. У мові C++ немає вбудованої підтримки регулярних виразів (можливо, це додадуть в наступних версіях мови C++), тому зазвичай це робиться шляхом перевірки кожного символу рядка на відповідність заданим критеріям.
З числовою валідацією ми зазвичай дбаємо про те, щоб число, яке ввів користувач, знаходилося в певному діапазоні (наприклад, від 0 до 20). Однак, на відміну від рядкової валідації, користувач може ввести дані, які взагалі не є числами, а нам потрібно буде обробляти і такі випадки.
Для вирішення цієї проблеми C++ надає ряд корисних функцій, які ми можемо використовувати для визначення того, чи є конкретні символи цифрами або буквами. Наступні функції знаходяться в заголовку cctype:
функція isalnum(int) — повертає ненульове значення, якщо параметром є буква або цифра;
функція isalpha(int) — повертає ненульове значення, якщо параметром є буква;
функція iscntrl(int) — повертає ненульове значення, якщо параметром є керуючий символ;
функція isdigit(int) — повертає ненульове значення, якщо параметром є цифра;
функція isgraph(int) — повертає ненульове значення, якщо параметром є символ, який виводиться (але не пробіл);
функція isprint(int) — повертає ненульове значення, якщо параметром є символ, який виводиться (включаючи пробіл);
функція ispunct(int) — повертає ненульове значення, якщо параметром не є ані буква, ані цифра, ані пробіл;
функція isspace(int) — повертає ненульове значення, якщо параметром є пробіл;
функція isxdigit(int) — повертає ненульове значення, якщо параметром є шістнадцяткова цифра (0-9, a-f, A-F).
Рядкова валідація
Давайте виконаємо просту рядкову валідацію, попросивши користувача ввести своє ім’я. Наші обмеження: ім’я може містити тільки букви і пробіли. Якщо користувач введе що-небудь інше, то ввід відхиляється. Перебирати і перевіряти ми будемо кожен символ користувацького вводу:
|
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 |
#include <iostream> #include <cctype> #include <string> int main() { while (1) { // Просимо користувача ввести своє ім'я std::cout << "Enter your name: "; std::string strName; std::getline(std::cin, strName); // вилучаємо цілий рядок, включаючи пробіли bool bRejected = false; // Перебираємо кожний символ рядка до тих пір, поки не дійдемо до кінця рядка або до відхилення символу for (unsigned int nIndex = 0; nIndex < strName.length() && !bRejected; ++nIndex) { // Якщо поточний символ є буквою, то все ок if (isalpha(strName[nIndex])) continue; // Якщо пробіл, то також ок if (strName[nIndex] == ' ') continue; // В протилежному випадку, відхиляємо весь користувацький ввід bRejected = true; } // Якщо користувацький ввід був прийнятий, то ми виходимо з циклу while, і програма завершує своє виконання. // В протилежному випадку, ми просимо користувача ввести своє ім'я ще раз if (!bRejected) break; } } |
Зверніть увагу, цей код не ідеальний: користувач може ввести в якості свого імені djfdfjkdjk jaaad fds або взагалі одні пробіли. Ми можемо покращити валідацію, уточнивши наші критерії перевірки: ім’я користувача повинно містити мінімум 1 символ і не більше 1 пробілу.
Тепер розглянемо інший випадок, коли ми просимо користувача ввести свій номер телефону. На відміну від імені користувача, номер телефону має фіксовану довжину. Отже, ми використовуватимемо інший підхід до валідації користувацького вводу. Ми напишемо функцію, яка перевірятиме номер телефону, який ввів користувач, на відповідність заздалегідь визначеному шаблону (такий ось своєрідний аналог регулярним виразам).
Шаблон працюватиме наступним чином:
# — будь-яка цифра в користувацькому вводі;
@ — будь-яка буква в користувацькому вводі;
_ — будь-який пробіл в користувацькому вводі;
? — взагалі будь-який символ.
Всі символи користувацького вводу і нашого шаблону повинні точно збігатися.
Отже, якщо ми хочемо, щоб користувацький ввід відповідав шаблону (###) ###-####, то користувач повинен ввести: ( три цифри ), пробіл, три цифри, тире і ще чотири цифри. Якщо щось з цього не співпаде, то користувацький ввід буде відхилено, наприклад:
|
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 |
#include <iostream> #include <string> bool InputMatches(std::string strUserInput, std::string strTemplate) { if (strTemplate.length() != strUserInput.length()) return false; // Перебираємо кожний символ користувацького вводу for (unsigned int nIndex = 0; nIndex < strTemplate.length(); nIndex++) { switch (strTemplate[nIndex]) { case '#': // = цифра if (!isdigit(strUserInput[nIndex])) return false; break; case '_': // = пробіл if (!isspace(strUserInput[nIndex])) return false; break; case '@': // = буква if (!isalpha(strUserInput[nIndex])) return false; break; case '?': // = взагалі будь-який символ break; default: // = точний збіг з символом if (strUserInput[nIndex] != strTemplate[nIndex]) return false; } } return true; } int main() { std::string strValue; while (1) { std::cout << "Enter a phone number (###) ###-####: "; std::getline(std::cin, strValue); // вилучаємо цілий рядок, включаючи пробіли if (InputMatches(strValue, "(###) ###-####")) break; } std::cout << "You entered: " << strValue << std::endl; } |
Використовуючи цю функцію, ми можемо змусити користувача ввести свій номер телефону точно по заданому нами шаблону. Але це не панацея на всі випадки життя.
Числова валідація
При роботі з числовим вводом очевидним шляхом розвитку подій є використання оператора вилучення для конвертації користувацького вводу в числовий тип. Перевіряючи failbit, ми можемо сказати, ввів користувач число чи ні. Наприклад:
|
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 |
#include <iostream> int main() { int nAge; while (1) { std::cout << "Enter your age: "; std::cin >> nAge; if (std::cin.fail()) // якщо ніякого вилучення не відбулося { std::cin.clear(); // то скидаємо всі поточні флаги стану і встановлюємо goodbit, щоб мати можливість використати функцію ignore() std::cin.ignore(32767, '\n'); // очищаємо потік від сміття continue; // просимо користувача ввести свій вік ще раз } if (nAge <= 0) // переконуємося, що nAge є додатнім числом continue; break; } std::cout << "You entered: " << nAge << std::endl; } |
Якщо користувач ввів число, то cin.fail() буде false, виконається оператор break, і ми вийдемо з циклу while. Якщо ж користувач ввів букву, то cin.fail() буде true, і користувачеві знову буде запропоновано ввести свій вік.
Однак, є один нюанс: якщо користувач введе рядок, який починається з цифр, але потім містить букви (наприклад, 53qwerty74), то перші цифри (53) будуть вилучені в nAge, а залишок рядка (qwerty74) залишиться у вхідному потоці, і failbit при цьому НЕ буде встановлений. Це загрожує наявністю сміття у вхідному потоці при наступному вилученні.
Давайте вирішимо цю проблему:
|
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 |
#include <iostream> int main() { int nAge; while (1) { std::cout << "Enter your age: "; std::cin >> nAge; if (std::cin.fail()) // якщо ніякого вилучення не відбулося { std::cin.clear(); // то скидаємо всі поточні флаги стану і встановлюємо goodbit, щоб мати можливість використати функцію ignore() std::cin.ignore(32767, '\n'); // очищаємо потік від сміття continue; // просимо користувача ввести свій вік ще раз } std::cin.ignore(32767, '\n'); // очищаємо все сміття, яке залишилося в потоці після вилучення if (std::cin.gcount() > 1) // якщо ми очистили більше одного символу continue; // то цей ввід вважається некоректним, і ми просимо користувача ввести свій вік ще раз if (nAge <= 0) // переконуємося, що nAge є додатнім числом continue; break; } std::cout << "You entered: " << nAge << std::endl; } |
Числова валідація за допомогою рядка
У вищенаведеному прикладі знадобилося чимало зусиль, щоб отримати одне просте значення! Інший спосіб обробки числового вводу полягає в тому, щоб прочитати користувацький ввід як рядок, обробити його як рядок і, якщо він пройде перевірку, конвертувати (цей рядок) в числовий тип. Наприклад:
|
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 |
#include <iostream> #include <sstream> // для stringstream int main() { int nAge; while (1) { std::cout << "Enter your age: "; std::string strAge; std::cin >> strAge; // Переконуємося, що кожний символ є цифрою bool bValid = true; for (unsigned int nIndex = 0; nIndex < strAge.length(); nIndex++) if (!isdigit(strAge[nIndex])) { bValid = false; break; } if (!bValid) continue; // На даний момент у нас є щось, що ми можемо конвертувати в число, // тому ми використовуємо stringstream для виконання конвертації std::stringstream strStream; strStream << strAge; strStream >> nAge; if (nAge <= 0) // переконуємося, що nAge є додатнім числом continue; break; } std::cout << "You entered: " << nAge << std::endl; } |
Чи буде цей варіант ефективнішим, ніж пряме числове вилучення, залежить від ваших параметрів валідації і обмежень.
Як ви можете бачити, валідація користувацького вводу в мові C++ займає не так вже й мало часу і зусиль. На щастя, багато подібних завдань (наприклад, виконання числової валідації за допомогою рядків) можна легко перетворити в функції, які потім можна буде повторно використовувати в інших програмах.
