JavaRush /Курси /C++ SELF /std::cin.clear() і <...

std::cin.clear() і std::cin.ignore()

C++ SELF
Рівень 20 , Лекція 1
Відкрита

1. Проблема

Коли ви пишете інтерактивну програму, цілком логічно очікувати від користувача розумної поведінки. Але користувач — істота творча: замість числа він може ввести котик, замість «виберіть пункт меню» — -999, а інколи просто натиснути Ctrl+D (EOF) і зникнути з вашого життя. Тож головна мета сьогодні — не «покарати користувача», а навчити програму повертатися до робочого стану.

Технічно це «залипання» виглядає так: ви один раз намагаєтеся зчитати число (std::cin >> n), але читання не вдається, потік переходить у стан fail(), і після цього всі наступні >> перестають просуватися. Потік ніби каже: «Я зламаний і більше нічого не читаю, доки мене не відновлять». Це як офісний принтер: зажував папір — і далі робить вигляд, ніби його не існує.

Мінідемонстрація того, як виникає «залипання»:

#include <iostream>

int main() {
    int x = 0;
    std::cin >> x; // якщо ввели "abc", потік перейде в fail()

    int y = 0;
    std::cin >> y; // уже не читає, бо потік "залип"

    std::cout << "fail=" << std::cin.fail() << '\n'; // fail=1
    std::cout << "x=" << x << " y=" << y << '\n';    // x=0 y=0 (найімовірніше)
}

Питання дня: як зробити так, щоб після некоректного введення потік знову почав читати, а сміття у вхідному буфері не спричиняло ту саму помилку знову й знову?

2. std::cin.clear() — скидаємо прапорці помилки

Ось важлива думка, з якої виростає половина всіх багів із серії «чому моє введення нескінченно лається». std::cin.clear() — це не «стерти те, що користувач увів». Це радше операція на кшталт «перемкнути світлофор потоку назад на зелений»: вона скидає прапорці помилок (наприклад, failbit, badbit — залежно від ситуації), щоб потік знову дозволив читання.

Але clear() принципово не чіпає вміст вхідного буфера. Якщо користувач увів abc\n, то після clear() ці a, b, c усе ще лежатимуть там само. Під час наступної спроби std::cin >> int ви знову намагатиметеся прочитати число, знову побачите a і знову отримаєте fail(). Тобто можна нескінченно лікувати симптоми, але так і не прибрати причину.

Невеликий приклад: «полагодили прапорець, але не прибрали сміття».

#include <iostream>

int main() {
    int n = 0;

    if (!(std::cin >> n)) {
        std::cout << "Некоректне введення\n";  // Некоректне введення
        std::cin.clear();                     // скинули fail()
    }

    // Якщо в буфері залишилися літери, наступне читання знову впаде
    if (!(std::cin >> n)) {
        std::cout << "Знову помилка\n";      // Знову помилка
    }
}

clear() — необхідна частина відновлення, але не достатня. Треба ще «викинути» некоректні символи з буфера. Для цього є ignore().

3. std::cin.ignore() — очищаємо буфер

Якщо clear() — це «зняти блокування», то ignore() — це «винести сміття». Саме разом вони дають стійку схему відновлення після помилки формату.

У спрощеному вигляді сигнатура така: ignore(count, delim). Потік пропускає символи, доки не станеться одне з двох: або буде пропущено count символів, або зустрінеться символ-роздільник delim (зазвичай '\n'). Ідея проста: після невдалого читання ми хочемо викинути все, що користувач набрав у цьому рядку, і перейти до наступної спроби вже з чистого місця.

Дуже типовий варіант, який ви бачитимете в хорошому навчальному коді:

std::cin.ignore(10000, '\n');

Це означає: «викинь до 10000 символів або до кінця рядка». Для навчальної задачі цього зазвичай достатньо, бо людина рідко друкує рядок завдовжки 10001 символ, хоча, якщо дуже постаратися…

Трохи канонічніший варіант, без магічного 10000, використовує std::numeric_limits:

#include <limits>
// ...
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

Ми беремо максимально можливу кількість символів типу std::streamsize і фактично кажемо: «викинь узагалі все до '\n'». Саме цьому підходу часто надають перевагу, бо він не залежить від принципу «вгадай число побільше».

До речі, ця тема настільки «слизька», що навіть у стандартизаційних обговореннях є окремі пункти про те, як зробити std::istream::ignore менш несподіваним. Це хороший сигнал: якщо вам здається, що ignore() інколи поводиться дивно, — ви не самі.

4. Ремонт введення: clear() + ignore()

Зараз ми зберемо «протокол ремонту» в одну зрозумілу модель. Тут важливо відчути порядок: спочатку ми помічаємо, що читання не вдалося; потім скидаємо стан помилки; далі викидаємо некоректне введення, щоб не натрапити на нього знову. Якщо поміняти кроки місцями або пропустити один із них, ви отримаєте вічний цикл «помилка → помилка → помилка». Виглядатиме це як компʼютер, який сперечається з користувачем до повної втрати сенсу життя.

Надійна логіка виглядає так:

#include <iostream>
#include <limits>

int main() {
    int n = 0;

    if (!(std::cin >> n)) {
        std::cout << "Введіть число\n"; // Введіть число
        std::cin.clear(); // 1) скинули fail()
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // 2) викинули рядок
    } else {
        std::cout << "n=" << n << '\n'; // наприклад: n=42
    }
}

Зверніть увагу на деталь: ми робимо ignore(..., '\n') саме до кінця рядка. Це важливо, бо користувач зазвичай вводить щось цілим рядком і натискає Enter. Нам потрібно викинути весь рядок повністю, щоб наступне введення почалося вже з чистого рядка.

Невеличка схема: що ламається і що лагодить

flowchart TD
    A[std::cin >> x] -->|успіх| B[Можна використовувати x]
    A -->|помилка формату| C["Потік переходить у стан fail()"]
    C --> D["std::cin.clear()"]
    D --> E["std::cin.ignore(..., '\n')"]
    E --> F[Можна знову зчитувати дані]

Цю схему корисно тримати в голові як коротку памʼятку проти «залипання» консольних програм.

5. >> і getline: звідки береться порожній рядок

Зараз буде тема, через яку новачки ненавидять getline, а getline у відповідь — новачків, хоча зазвичай винен не getline, а наші очікування. Проблема така: оператор >> зчитує токени й залишає символ переведення рядка '\n' у буфері. А std::getline читає до кінця рядка, тож якщо перший символ у буфері — саме '\n', він чесно вважає, що рядок порожній, і повертає порожній рядок.

Тобто типовий баг виглядає так:

#include <iostream>
#include <string>

int main() {
    int age = 0;
    std::string name;

    std::cin >> age;              // ввели: 20<Enter>
    std::getline(std::cin, name); // name = "" (порожньо!)

    std::cout << "age=" << age << " name=" << name << '\n';
}

Щоб getline не «зʼїв» залишений символ переведення рядка, ми один раз викликаємо ignore() після >>:

#include <iostream>
#include <limits>
#include <string>

int main() {
    int age = 0;
    std::string name;

    std::cin >> age;
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // зʼїли '\n'

    std::getline(std::cin, name);
    std::cout << "age=" << age << " name=" << name << '\n'; // age=20 name=Alice
}

Важливо розуміти саму ідею: ignore() тут — не «ремонт після помилки», а «гігієна буфера» під час зміни режиму введення. Ми не лагодимо fail(), а просто прибираємо хвіст, який заважає getline.

6. Прибираємо копіпасту: функції для ремонту та читання int

Коли ви один раз зрозуміли звʼязку clear()+ignore(), виникає нова спокуса: вставити її у 20 місць коду. За тиждень ви забудете, навіщо там ignore, за два — заміните 10000 на 5000 «бо так красивіше», а за три — шукатимете баг у місці, де забули clear(). Тому дуже корисно оформити ремонт потоку й читання числа як невеликі функції.

Спочатку зробімо функцію, яка «зʼїдає залишок рядка». Вона зручна і для ремонту, і для переходу до getline:

#include <iostream>
#include <limits>

void discard_line() {
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

Тепер функція «полагодити після fail»:

#include <iostream>
#include <limits>

void repair_after_fail() {
    std::cin.clear(); // скинути failbit
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // викинути сміття до кінця рядка
}

І нарешті — корисна «обгортка» для читання числа з контрактом bool:

#include <iostream>
#include <limits>

bool read_int(int& out) {
    if (std::cin >> out) {
        return true;
    }
    repair_after_fail();
    return false;
}

Тут контракт дуже простий: або true, і out містить коректне значення, або false, і потік уже приведено в робочий стан для наступної спроби. Це саме те, що потрібно для інтерактивного меню.

7. Мініприклад: меню зі списком покупок

Щоб тема не залишилася «двома рядками, які треба завчити», давайте вплетемо її в маленький застосунок. Уявімо, що в нас є список покупок: ми можемо додати товар, показати список і вийти. Пункти меню обираються числом, а назва товару вводиться рядком. Саме тут і зустрічаються >>, getline та потенційні помилки.

Почнімо з простого каркаса меню, поки що без стійкого введення:

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> items;

    std::cout << "Список покупок\n";      // Список покупок
    std::cout << "1) Додати товар\n";     // 1) Додати товар
    std::cout << "2) Показати\n";         // 2) Показати
    std::cout << "0) Вийти\n";            // 0) Вийти

    int cmd = 0;
    std::cin >> cmd;

    // далі буде обробка cmd...
}

Тепер додамо наші функції ремонту та безпечного читання команди:

#include <iostream>
#include <limits>

void repair_after_fail() {
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}

bool read_menu_cmd(int& out_cmd) {
    if (std::cin >> out_cmd) {
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); // прибрали хвіст рядка
        return true;
    }
    repair_after_fail();
    return false;
}

Зверніть увагу: після успішного читання команди ми теж робимо ignore(). Не тому, що «так треба завжди», а тому, що далі ми плануємо читати рядки через getline і хочемо, щоб наступний getline не зустрів '\n'.

Тепер основний цикл меню стає набагато спокійнішим:

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> items;

    while (true) {
        std::cout << "1) Додати товар, 2) Показати, 0) Вийти\n"; // 1) Додати товар, 2) Показати, 0) Вийти

        int cmd = -1;
        if (!read_menu_cmd(cmd)) {
            std::cout << "Введіть число (0, 1, 2)\n"; // Введіть число (0, 1, 2)
            continue;
        }

        if (cmd == 0) break;

        if (cmd == 1) {
            std::string name;
            std::cout << "Назва товару: "; // Назва товару:
            std::getline(std::cin, name);

            if (!name.empty()) {
                items.push_back(name);
            }
        } else if (cmd == 2) {
            std::cout << "Товари:\n"; // Товари:
            for (std::size_t i = 0; i < items.size(); ++i) {
                std::cout << i + 1 << ") " << items[i] << '\n'; // 1) Молоко ...
            }
        } else {
            std::cout << "Невідома команда\n"; // Невідома команда
        }
    }
}

Зауважте, як тут проявляється наша мета: програма не «вмирає» від котик замість числа. Вона каже «введіть число», очищає потік і продовжує працювати. У цьому й полягає практична стійкість консольного введення.

8. fail, eof, bad: що з ними робити

У цій лекції легко відчути хибну впевненість: «ага, якщо щось пішло не так — завжди clear()+ignore()». На практиці є нюанс: fail() часто означає помилку формату, і це справді можна лагодити, бо користувач може з другої спроби ввести дані правильно. Але eof() означає, що введення більше немає, наприклад користувач закрив потік введення. У такому разі намагатися «прочитати ще раз» зазвичай безглуздо — дані просто не зʼявляться.

Тому в серйознішій програмі після циклу читання варто розрізняти ситуацію «помилка формату» і ситуацію «кінець введення». Якщо ми робимо інтерактивне меню, то EOF зазвичай трактуємо як «вихід» — без сварок та істерик.

А bad() у базовій моделі вважаємо станом «потік справді зламаний». Для звичайної консолі це рідкість, але якщо таке сталося, лагодити його «нашими заклинаннями» найчастіше вже не потрібно. У навчальних задачах у такій ситуації можна просто завершити роботу програми.

9. Типові помилки при clear() і ignore()

Помилка №1: робити clear(), але не робити ignore().
Це найпопулярніший сценарій із серії «чому моя програма зациклилася». Ви скидаєте прапорець fail(), думаєте, що потік «виправився», але в буфері все ще лежить те саме сміття, яке й спричинило помилку. У підсумку наступне читання знову читає ті самі символи й знову падає. Ремонт має бути парним: скинули помилку — і викинули некоректне введення до кінця рядка.

Помилка №2: робити ignore() без clear() після fail().
Іноді здається: «ну я ж викину сміття, і все буде нормально». Але якщо потік перебуває в стані fail(), то багато операцій введення не виконуватимуться так, як ви очікуєте, бо потік «заблокований». Тому логіка ремонту після помилки формату зазвичай починається з clear(), а вже потім іде очищення буфера.

Помилка №3: плутати два різні застосування ignore(): «ремонт» і «гігієна перед getline».
ignore() потрібен і після помилки формату, і після успішного >>, якщо далі ви викликаєте getline. Ці випадки дуже схожі зовні, але мають різний зміст: у першому ви рятуєте програму після некоректного введення, у другому — просто прибираєте залишений '\n', щоб getline не повернув порожній рядок. Якщо цього не розрізняти, легко вставляти ignore() «куди завгодно» й отримувати дивні ефекти.

Помилка №4: ставити надто малий ліміт у ignore() і залишати частину сміття.
Якщо ви пишете std::cin.ignore(5, '\n'), а користувач увів рядок із 20 символів, то ви викинете лише перші 5, а решта 15 і далі лежатимуть у буфері й можуть знову ламати наступне введення. У навчальних програмах майже завжди краще використовувати або дуже велике число, або std::numeric_limits<std::streamsize>::max(), щоб чесно викинути залишок рядка повністю.

Помилка №5: намагатися «лагодити» EOF так само, як помилку формату.
EOF — це не «поганий формат», а ситуація, коли дані закінчилися. Якщо користувач закрив введення, ваша програма не має нескінченно просити «введіть ще раз» — вона має коректно завершитися або вийти з режиму введення. Тому clear()+ignore() — це інструмент саме для помилок формату (fail()), а не універсальна кнопка «зроби добре».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ