1. Вступ
Коли ви пишете умову в if, то, по суті, розповідаєте компʼютеру маленьку логічну історію: «якщо вік більший або дорівнює 18 і команда не "exit", то…». Проблема в тому, що компʼютер читає цю історію не так, як людина. Він читає її за строгими правилами синтаксису: які частини «склеюються» раніше, а які — пізніше. Саме ці правила й називають пріоритетом операторів.
Пріоритет вигадали не зі шкідливості. Він потрібен, щоб не доводилося щосекунди обгортати вирази в дужки (інакше ми писали б майже як у математиці для першокласників: «спочатку помнож, потім додай»). Але за цю зручність доводиться платити: інколи вираз здається людині «очевидним», а компілятор групує його по‑іншому.
Корисний факт: навіть у формальному описі мови, тобто в граматиці виразів, операції з вищим пріоритетом «розташовуються» як більш «внутрішні» конструкції. Тому стандарт намагається впорядкувати правила так, щоб високопріоритетні частини стояли раніше.
Вам не потрібно читати стандарт (бережіть психіку), але сама ідея важлива: компілятор завжди групує вираз за правилами, а не за настроєм.
Пріоритети, які потрібні зараз
Зараз ми не будемо вчити «весь пріоритет C++» як молитву. Нам достатньо компактного набору, який найчастіше ламає умови в новачків. На нашому рівні, тобто на рівні порівнянь і логіки, можна запамʼятати таку драбинку:
- Спочатку застосовуються унарні операції на кшталт ! (логічне НЕ).
- Потім виконується арифметика (* сильніший за +, це ви вже бачили на числах).
- Потім ідуть порівняння (<, <=, >, >=, ==, !=) — вони перетворюють «числа/рядки» на bool.
- Потім іде логічне І: &&.
- Потім — логічне АБО: ||.
Тобто, якщо дуже коротко: ! сильніший за порівняння, порівняння сильніші за &&, а && сильніший за ||.
Щоб це було наочніше, ось маленька табличка лише з нашим набором:
| Оператори | Приклад | Що виходить |
|---|---|---|
|
|
інвертує bool |
| Порівняння | |
дають bool |
|
|
«обидва мають бути true» |
|
|
«хоча б один true» |
І головний практичний висновок: якщо ви змішали && і || в одній умові, але не поставили дужок, компілятор «вважає», що && треба виконати раніше. Іноді це збігається з вашим задумом, а іноді — ні.
2. Дужки — це коментар для мозку
Дужки в умовах новачки часто сприймають за принципом «ну гаразд, аби компілятор не сварився». Насправді компілятор у 90 % випадків і без дужок усе чудово зрозуміє. Дужки потрібні не компілятору. Дужки потрібні людині, яка читатиме код. І, спойлер: за тиждень цією людиною будете ви самі, тільки вже без спогадів про те, «що я мав на увазі».
Коли ви ставите дужки, то робите дві речі. По‑перше, фіксуєте зміст: навіть якщо ви забули пріоритет, дужки його перевизначають. По‑друге, створюєте «візуальні змістові блоки»: читач бачить, що порівняння — це окремі цеглинки, а &&/|| — клей між цими цеглинками.
Порівняйте два варіанти. Обидва коректні, але другий читається як нормальне речення.
#include <iostream>
int main() {
int x = 7;
bool ok1 = x >= 1 && x <= 10;
bool ok2 = (x >= 1) && (x <= 10);
std::cout << ok1 << ' ' << ok2 << '\n'; // 1 1
}
У першому варіанті доводиться «тримати в голові», де закінчується одне порівняння й починається інше. У другому — ви просто бачите: «дві перевірки, поєднані І».
3. Часті пастки пріоритету
Зараз буде кілька типових пасток. Я спеціально покажу їх на коротких прикладах, бо довгий приклад зазвичай маскує проблему: ви дивитеся на 30 рядків, а помилка ховається в одних дужках.
Пастка ! і порівняння: інвертували не те
Дуже поширена помилка — хотіти написати «не дорівнює нулю», але випадково отримати інше групування: «спочатку НЕ, потім порівняння».
#include <iostream>
int main() {
int x = 0;
bool a = !x == 0; // читається людьми як "x не дорівнює 0" (але це не так)
bool b = !(x == 0); // "НЕ (x дорівнює 0)" — ось це те, що зазвичай мали на увазі
std::cout << a << ' ' << b << '\n'; // 0 0
}
Чому це пастка? Тому що ! застосовується до найближчого виразу. У !x == 0 спочатку вийде !x (тобто !0, а це true → 1), а потім — порівняння 1 == 0, тобто false. І виглядає це так, ніби «щось працює», хоча сенс уже змінився.
Практичне правило: якщо ви хочете інвертувати складену умову, дужки обовʼязкові: !(...).
Пастка && і ||: компілятор групує інакше
Уявімо, що ми вирішуємо, чи пускати користувача. Нехай доступ дозволено, якщо він адмін, або якщо він модератор і має ключ.
#include <iostream>
int main() {
bool is_admin = false;
bool is_moderator = true;
bool has_key = false;
bool can_enter = is_admin || is_moderator && has_key;
std::cout << can_enter << '\n'; // 0
}
Компілятор прочитає це так:
is_admin || (is_moderator && has_key)
тому що && сильніший за ||. Тут усе правильно, але проблема в іншому: читач може прочитати це так:
(is_admin || is_moderator) && has_key
і отримати інший зміст: «або адмін, або модератор — але ключ потрібен усім». А це вже інша політика доступу.
Тому в таких умовах корисно ставити дужки навіть тоді, коли ви памʼятаєте пріоритет:
#include <iostream>
int main() {
bool is_admin = false;
bool is_moderator = true;
bool has_key = false;
bool can_enter = is_admin || (is_moderator && has_key);
std::cout << can_enter << '\n'; // 0
}
Тут дужки працюють як дорожні знаки: «увага, я так задумав».
Чому запис a < b < c — майже завжди помилка
У математиці ви звикли, що a < b < c означає «a менше за b і b менше за c». У C++ це не так. У C++ вираз a < b спочатку обчислюється і дає bool, а потім цей bool порівнюється з c. Тобто виходить приблизно «(a < b) < c». А bool — це 0 або 1, і порівняння перетворюється на дуже дивну перевірку.
Подивімося на приклад:
#include <iostream>
int main() {
int a = 5;
int b = 10;
int c = 7;
bool wrong = (a < b < c); // компілюється, але зміст "уплив"
bool right = (a < b) && (b < c); // те, що мали на увазі
std::cout << wrong << ' ' << right << '\n'; // 1 0
}
Чому wrong став 1? Тому що a < b — це true, тобто 1, а 1 < 7 — це true. У результаті перевірка проходить «магічним» чином, і такі баги потім дуже важко помітити на око.
Правильний спосіб — дві перевірки й &&: (a < b) && (b < c).
Пастка під час виведення: std::cout << x == y
Це вже не зовсім про умови, але таке трапляється постійно, особливо коли ви налагоджуєте програму за допомогою друку (а на початковому етапі курсу ми саме так і робимо).
Новачок хоче вивести «x дорівнює y?» і пише:
#include <iostream>
int main() {
int x = 3;
int y = 5;
std::cout << x == y << '\n'; // помилка компіляції (або дуже дивні повідомлення)
}
Чому так? Тому що оператор << (виведення) звʼязується сильніше, ніж == у цьому контексті, і компілятор намагається зрозуміти це так:
(std::cout << x) == y
А (std::cout << x) — це не число, а потік, і порівнювати потік із числом безглуздо.
Правильно писати так:
#include <iostream>
int main() {
int x = 3;
int y = 5;
std::cout << (x == y) << '\n'; // 0
}
Ці дужки — не прикраса. Вони буквально означають: «спочатку порівняй, потім виведи результат».
5. Прийоми для читабельних умов
Даємо імʼя частинам умови: менше дужок, більше змісту
Іноді умова стає довгою не тому, що ви погано пишете, а тому, що така вже реальність: потрібно перевірити кілька речей. І саме тут дужки можуть розростися, як виноградна лоза.
У цей момент допомагає простий прийом: винести частини умови в bool‑змінні зі зрозумілими іменами. Це не «зайві рядки», а інвестиція в читабельність.
Уявімо, що в нас є маленький консольний застосунок, який приймає команду й число. Припустімо, команда може бути "set" або "show", а число має бути в діапазоні 1–10.
#include <iostream>
#include <string>
int main() {
std::string cmd;
int value = 0;
std::cin >> cmd >> value;
bool is_known_cmd = (cmd == "set") || (cmd == "show");
bool value_ok = (value >= 1) && (value <= 10);
if (is_known_cmd && value_ok) {
std::cout << "ok\n"; // ok
} else {
std::cout << "bad\n"; // bad (якщо щось не підходить)
}
}
Тут важлива думка: тепер ви читаєте if (is_known_cmd && value_ok) майже як звичайний текст. А подробиці умов «живуть» у рядках вище й мають власні імена.
Розбір умови покроково: як компілятор її бачить
Зараз ми зробимо те, що корисно робити подумки, коли ви сумніваєтеся: уявімо умову як дерево групування.
Візьмемо умову: cmd == "exit" || cmd == "quit" && tries < 3
Через пріоритет && це дорівнює: (cmd == "exit") || ((cmd == "quit") && (tries < 3))
Тобто команда "exit" спрацює завжди, а "quit" — лише якщо спроб менше за 3. Можливо, саме цього ви й хотіли. А можливо, ви мали на увазі «exit або quit, і при цьому tries < 3», тобто: ((cmd == "exit") || (cmd == "quit")) && (tries < 3)
Саме заради таких ситуацій корисно або ставити дужки за змістом, або виносити умови в bool‑змінні.
Для наочності (спрощено) це можна подати схемою:
flowchart TD
A[Велика умова] --> B{"||"}
B --> C[cmd == 'exit']
B --> D{&&}
D --> E[cmd == 'quit']
D --> F[tries < 3]
Якщо ви хочете інший зміст, просто змінюєте «корінь» дерева дужками.
6. Практика: міні‑консоль команд без ребусів
Уявімо, що ми продовжуємо навчальний застосунок «міні‑консоль команд»: користувач вводить команду й параметри. Поки що без циклів: один запуск, одна команда, одне рішення. Нехай команди такі: "login" і "help". Для "login" ми хочемо прийняти імʼя (без пробілів) і вік. Увійти можна, якщо імʼя не порожнє (на нашому рівні — name.size() > 0) і вік у діапазоні [18, 120]. Для "help" просто друкуємо підказку.
Ось версія, де спеціально багато дужок «за змістом»:
#include <iostream>
#include <string>
int main() {
std::string cmd;
std::cin >> cmd;
if (cmd == "help") {
std::cout << "commands: help, login\n";
} else if (cmd == "login") {
std::string name;
int age = 0;
std::cin >> name >> age;
if ((name.size() > 0) && (age >= 18) && (age <= 120)) {
std::cout << "welcome\n";
} else {
std::cout << "denied\n";
}
} else {
std::cout << "unknown command\n";
}
}
Тут дужки не обовʼязкові для компілятора, але вони роблять структуру прозорою: кожне порівняння — окрема цеглинка.
А ось версія з іменованими булевими прапорцями, де дужок зазвичай менше, але зміст ще простіший:
#include <iostream>
#include <string>
int main() {
std::string cmd;
std::cin >> cmd;
if (cmd == "login") {
std::string name;
int age = 0;
std::cin >> name >> age;
bool name_ok = (name.size() > 0);
bool age_ok = (age >= 18) && (age <= 120);
if (name_ok && age_ok) {
std::cout << "welcome\n";
} else {
std::cout << "denied\n";
}
}
}
Зверніть увагу: ми не додали «нових технологій», а просто зробили вже знайомі речі читабельнішими.
7. Типові помилки під час роботи з пріоритетом і дужками
Помилка № 1: «Економія дужок» перетворює умову на ребус.
Дуже легко потрапити в ситуацію, коли умова «формально коректна», але ви (або ваш одногрупник, або ви в майбутньому) читаєте її 40 секунд, а потім усе одно не впевнені в змісті. Дужки в умовах — це не зайвий шум, а документація намірів. Якщо сумніваєтеся — ставте дужки за змістовими блоками.
Помилка № 2: Інвертували ! не те, що хотіли.
Коли ви пишете !x == 0 або !a < b, то зазвичай хочете сказати «не дорівнює» або «не менше». Але ! застосовується до найближчого виразу, і ви отримуєте несподіване групування. Щоб інвертувати складену умову, використовуйте !(...), а «не дорівнює» пишіть просто через != — так і простіше, і чесніше.
Помилка № 3: Змішали && і || без дужок і почали вірити очам, а не правилам.
Така умова може бути коректною, але її часто читають не так, як вона працює насправді. Навіть якщо ви памʼятаєте, що && сильніший за ||, читач може цього не памʼятати. Дужки навколо логічних груп роблять політику «або/і» явною, особливо в перевірках доступу, діапазонів і команд.
Помилка № 4: Пишуть «математичний ланцюжок» a < b < c.
У C++ це не ланцюжок порівнянь, а два різні порівняння, де результат першого (bool) бере участь у другому. Це дає «магічні» істинні результати. Правильна форма — (a < b) && (b < c).
Помилка № 5: Намагаються вивести порівняння без дужок: std::cout << x == y.
Через пріоритет операторів компілятор сприймає це як спробу порівняти потік із числом. Виводьте порівняння лише так: std::cout << (x == y), і це правило заощаджує багато часу й нервів.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ