JavaRush /Курси /C++ SELF /Типові помилки: = vs ==, перевірки введення

Типові помилки: = vs ==, перевірки введення

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

1. Помилка століття: = замість ==

Виглядає як дрібниця: один знак рівності чи два. Але за змістом це дві зовсім різні дії. == — це запитання «чи дорівнює?», а = — команда «поклади це значення в змінну».

Хитрість у тому, що присвоєння (=) теж є виразом: воно повертає присвоєне значення. Тому компілятор часто не мусить сигналізувати про проблему: він бачить умову, яка обчислюється в число, а число в умові трактується так: «0 — хиба, не 0 — істина».

Уявіть, що ви на іспиті замість запитання «Це правда?» кажете: «Зроби це правдою!». Викладач здивується, але формально фраза все одно побудована правильно. Оператор = у if — приблизно такий самий трюк.

Мінідемо: як помилка ламає і дані, і логіку

Нижче — приклад, який компілюється, але працює не так, як ви очікуєте:

#include <iostream>

int main() {
    int x = 5;

    if (x = 0) {                 // ПОМИЛКА: присвоєння замість порівняння
        std::cout << "x дорівнює нулю\n";
    }

    std::cout << x << '\n';      // 0
}

Ось що тут відбувається: x = 0 присвоює нуль, вираз має значення 0, умова хибна, гілка if не виконується, але x уже зіпсовано.

Правильний варіант

#include <iostream>

int main() {
    int x = 5;

    if (x == 0) {                // порівняння
        std::cout << "x дорівнює нулю\n";
    } else {
        std::cout << "x не дорівнює нулю\n"; // x не дорівнює нулю
    }
}

Чому компілятор не завжди рятує

Тому що з погляду мови це допустимо: умова if (...) має обчислитися в щось, що можна звести до bool. Число до bool зводиться. Отже, формально все коректно.

Так, багато компіляторів показують попередження (warning), але це не гарантія, особливо в навчальних веб-IDE, де налаштування можуть бути мʼякшими. Тому ваша страховка — уважно стежити за оператором усередині умови.

Звичка: порівнюємо явно, без «натяків»

Писати «хитро» — погана ідея для новачка:

if (x) { /* ... */ } // працює, але на початку читається гірше

Хороша ідея — писати прямо:

if (x != 0) { /* ... */ }

У навчальному коді це майже завжди читається краще. А читабельність — це вже мінус половина помилок.

2. Перевірка введення: if (std::cin >> x)

Коли ви читаєте число через std::cin >> x, то мовчки припускаєте, що користувач введе саме число. Але користувач — істота творча. Він може ввести qwe, може не там натиснути Enter, може вставити 10.5 замість 10. І тоді std::cin перейде в стан помилки: читання не вдасться, змінна може залишитися зі старим значенням, а наступні читання почнуть поводитися дивно.

Поки що ми детально не розбираємо стани потоку (fail, bad, clear, ignore) — це буде в інших темах. Сьогодні нам достатньо мінімального підходу: перевірити сам факт читання.

Найпростіший і найкорисніший шаблон

#include <iostream>

int main() {
    int n = 0;

    if (std::cin >> n) {
        std::cout << "прочитано: " << n << '\n';   // наприклад: прочитано: 42
    } else {
        std::cout << "помилка введення\n";          // якщо ввели не число
    }
}

В умові if (std::cin >> n) відбуваються дві речі: спочатку програма намагається прочитати число, а потім перевіряє, чи вдалося це зробити. Це компактно й на вашому поточному рівні — ідеально.

Мініприклад: команда і число без надії на те, що «якось прочитається»

Розвиньмо ідею простого «консольного помічника», який читає команду й виконує дію. Поки що без циклів: один запуск — одна команда.

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "square") {
        int x = 0;
        if (std::cin >> x) {
            std::cout << x * x << '\n';       // наприклад: 25
        } else {
            std::cout << "помилка введення\n";
        }
    }
}

Зверніть увагу: перевірка введення стоїть саме там, де вона потрібна, — поруч із використанням числа. Ми не просто сподіваємося, що його вдалося прочитати.

3. Граничні випадки: вік, діапазони та рядки

Межі — це значення, на яких програма найчастіше спотикається: 0, 1, -1, «рівно 18», порожній рядок, останній символ рядка, ділення на нуль. Новачок зазвичай тестує «середину» (наприклад, 20 років), а помилка ховається на «краях» (18 років).

Тому корисно проговорити умову словами й перевірити, де саме проходить межа: входить це значення в умову чи ні.

Щоб мислити простіше, тримайте в голові таке правило: будь-який доступ за індексом і будь-яке ділення потребують захисту. І навіть якщо вам «здається», що там усе буде коректно, практика швидко покаже: так здається всім.

> vs >=: «включно з межею» — це окреме рішення

Порівняйте дві перевірки:

#include <iostream>

int main() {
    int age = 18;

    std::cout << (age > 18)  << '\n';  // 0
    std::cout << (age >= 18) << '\n';  // 1
}

Логіка «повнолітній з 18» вимагає >=, а не >. Це не питання стилю, а питання змісту.

Більш життєвий фрагмент:

#include <iostream>

int main() {
    int age = 0;
    std::cin >> age;

    if (age >= 18) {
        std::cout << "повнолітній\n";
    } else {
        std::cout << "неповнолітній\n";
    }
}

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

Діапазон «від L до R включно»: пишемо через &&

Якщо умова звучить як «x від 1 до 10», це майже завжди означає: «і не менше 1, і не більше 10».

#include <iostream>

int main() {
    int x = 0;
    std::cin >> x;

    if (x >= 1 && x <= 10) {
        std::cout << "у діапазоні\n";
    } else {
        std::cout << "поза діапазоном\n";
    }
}

Межа — частина задачі. Важливо не просто «написати if», а вирішити, що робити на 1 і 10.

Порожній рядок: перш ніж брати s[0], переконайтеся, що рядок не порожній

Рядок — це не гарантія того, що всередині є хоча б один символ. Якщо ви прочитали рядок через std::getline, користувач міг просто натиснути Enter.

#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    if (s.size() == 0) {
        std::cout << "порожній\n";
    } else {
        std::cout << s[0] << '\n';      // виводимо перший символ
    }
}

Тут перевірка «порожньо/не порожньо» — це не занудство. Саме вона відділяє програму від виходу за межі рядка.

Останній символ: s[s.size() - 1] потребує ще більшої уваги

Тому що якщо s.size() == 0, вираз s.size() - 1 дає некоректний індекс.

#include <iostream>
#include <string>

int main() {
    std::string s;
    std::getline(std::cin, s);

    if (s.size() > 0) {
        char last = s[s.size() - 1];
        std::cout << last << '\n';
    } else {
        std::cout << "порожній\n";
    }
}

Зверніть увагу: перевірка тут > 0, а не >= 0 (другий варіант беззмістовний). І так, size() для порожнього рядка дорівнює 0.

4. Захисні перевірки: ділення й коротке замикання

Ділення на нуль — класична проблема. Особливо прикро, що вона часто зʼявляється не тому, що ви хотіли ділити на нуль, а тому, що не врахували: користувач може ввести 0. І він справді може. Навіть якщо ви просили «введіть дільник», користувач усе одно може ввести нуль.

Коротке замикання в && — той рідкісний випадок, коли «лінощі» оператора роблять програму безпечнішою: якщо ліва частина хибна, права навіть не обчислюється. Отже, якщо ліворуч стоїть перевірка безпеки, праворуч можна ставити потенційно небезпечну дію.

Правильний порядок частин у &&

#include <iostream>

int main() {
    int a = 0;
    int b = 0;
    std::cin >> a >> b;

    if (b != 0 && (a / b > 2)) {
        std::cout << "a/b > 2\n";
    } else {
        std::cout << "умова хибна\n";
    }
}

Якщо b == 0, ліва частина b != 0 хибна, і ділення не відбудеться. Ось що таке захисна перевірка.

Неправильний порядок (небезпечно)

#include <iostream>

int main() {
    int a = 0;
    int b = 0;
    std::cin >> a >> b;

    if ((a / b > 2) && b != 0) {        // НЕБЕЗПЕЧНО: ділення вже відбулося
        std::cout << "a/b > 2\n";
    }
}

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

5. Практичний мініприклад: одна команда — одна дія

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

Ідея проста: користувач вводить команду (divide, first, last), а потім — потрібні дані.

Каркас розгалуження за командою

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "divide") {
        std::cout << "режим: divide\n";
    } else if (cmd == "first") {
        std::cout << "режим: first\n";
    } else {
        std::cout << "невідома команда\n";
    }
}

Це базове розгалуження: команда — це рядок, порівняння рядків дає bool, а if/else if/else обирає одну гілку.

Команда divide: перевіряємо введення й захищаємо ділення

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "divide") {
        int a = 0, b = 0;
        if (std::cin >> a >> b) {
            if (b != 0) std::cout << (a / b) << '\n';
            else std::cout << "ділення на нуль\n";
        } else {
            std::cout << "помилка введення\n";
        }
    }
}

Тут одразу дві лінії захисту: спочатку ми переконуємося, що числа взагалі вдалося прочитати, а потім — що дільник не дорівнює нулю.

Команда first: читаємо рядок і перевіряємо порожнечу

З командою first є нюанс: після std::cin >> cmd у буфері може залишитися символ нового рядка, і getline може прочитати порожній рядок. Ми докладно говорили про це в темі про getline, тож тут просто застосуємо акуратний прийом: читати рядок через std::getline(std::cin >> std::ws, s)std::ws «зʼїсть» пробіли й переведення рядка перед реальним текстом.

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "first") {
        std::string s;
        std::getline(std::cin >> std::ws, s);

        if (s.size() == 0) std::cout << "порожній\n";
        else std::cout << s[0] << '\n';
    }
}

Так, це виглядає трохи «магічно» через std::ws, але ви вже бачили проблему «порожнього рядка після >>», і це одне зі стандартних рішень.

Команда last: останній символ без виходу за межі

#include <iostream>
#include <string>

int main() {
    std::string cmd;
    std::cin >> cmd;

    if (cmd == "last") {
        std::string s;
        std::getline(std::cin >> std::ws, s);

        if (s.size() > 0) std::cout << s[s.size() - 1] << '\n';
        else std::cout << "порожній\n";
    }
}

Зміст той самий: спочатку перевірка, потім індексування.

Схема «як мислити» про небезпечні місця

Коли ви пишете гілку if, корисно подумки пройтися таким маршрутом:

flowchart TD
    A[Потрібно виконати дію] --> B{Потрібні вхідні дані?}
    B -->|Так| C{Введення успішне?}
    C -->|Ні| X[Повідомляємо про помилку]
    C -->|Так| D{Є небезпечна операція?}
    D -->|Ділення/індекс| E{Захисну перевірку виконано?}
    E -->|Ні| Y[Повідомляємо і не виконуємо дію]
    E -->|Так| F[Виконуємо дію]
    B -->|Ні| F

Ця блок-схема — не «формалізм», а просто спосіб не забувати про перевірки, особливо коли код починає розгалужуватися.

6. Типові помилки

Помилка № 1: = замість == в умові.
Це одна з найнеприємніших помилок, тому що вона часто не заважає компіляції. = змінює змінну й повертає присвоєне значення, тому умова може стати «хибною» або «істинною» не через порівняння, а через те, що ви щойно щось перезаписали. Уникнути її допомагає звичка уважно стежити за «центральним оператором» умови й писати порівняння явно: x == 0, cmd == "exit".

Помилка № 2: переплутана межа (> замість >=, < замість <=).
Такі помилки майже завжди проявляються лише на одному значенні: 18, 0, «останній індекс». Тому вони живуть довго: ви тестуєте 20, 30, 40 — і все працює. Звичка, яка допомагає, може звучати по-дитячому, але працює безвідмовно: спочатку сказати умову словами («включно з 18»), а вже потім вибрати знак.

Помилка № 3: індексування рядка без перевірки довжини.
std::string не зобовʼязаний бути непорожнім. Якщо ви пишете s[0] або s[s.size() - 1], спочатку має стояти умова s.size() > 0. Особливо підступний «останній символ», тому що вираз s.size() - 1 виглядає нешкідливо, але на порожньому рядку перетворюється на некоректний індекс. Захист — це окрема гілка if або лівий аргумент у &&.

Помилка № 4: ділення без перевірки нуля (або перевірка стоїть «праворуч»).
Перевірка b != 0 має стояти до ділення. Найзручніший шаблон — b != 0 && (a / b > ...), де коротке замикання гарантує, що ділення не відбудеться при b == 0. Якщо поміняти частини умови місцями, ви отримаєте небезпечну поведінку, хоча «перевірка» начебто є.

Помилка № 5: відсутність мінімальної перевірки введення.
Якщо ви пишете std::cin >> x; і відразу використовуєте x, ви припускаєте, що введення коректне. На практиці це призводить до дивної поведінки й неправильних результатів, коли користувач вводить не число. Навіть без заглиблення в будову потоків уже можна писати надійніше: if (std::cin >> x) { ... } else { ... }. Це додає кілька рядків, але економить години здивування.

1
Опитування
Умовні оператори і логіка, рівень 3, лекція 5
Недоступний
Умовні оператори і логіка
Умовні оператори і логіка
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ