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 { ... }. Це додає кілька рядків, але економить години здивування.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ