JavaRush /Курси /C++ SELF /Пріоритет операторів і дужки

Пріоритет операторів і дужки

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

1. Вступ

Коли ви пишете умову в if, то, по суті, розповідаєте компʼютеру маленьку логічну історію: «якщо вік більший або дорівнює 18 і команда не "exit", то…». Проблема в тому, що компʼютер читає цю історію не так, як людина. Він читає її за строгими правилами синтаксису: які частини «склеюються» раніше, а які — пізніше. Саме ці правила й називають пріоритетом операторів.

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

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

Пріоритети, які потрібні зараз

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

  1. Спочатку застосовуються унарні операції на кшталт ! (логічне НЕ).
  2. Потім виконується арифметика (* сильніший за +, це ви вже бачили на числах).
  3. Потім ідуть порівняння (<, <=, >, >=, ==, !=) — вони перетворюють «числа/рядки» на bool.
  4. Потім іде логічне І: &&.
  5. Потім — логічне АБО: ||.

Тобто, якщо дуже коротко: ! сильніший за порівняння, порівняння сильніші за &&, а && сильніший за ||.

Щоб це було наочніше, ось маленька табличка лише з нашим набором:

Оператори Приклад Що виходить
!
!ok
інвертує bool
Порівняння
x <= 10, s == "exit"
дають bool
&&
a && b
«обидва мають бути true»
||
a || b
«хоча б один 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, а це true1), а потім — порівняння 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", а число має бути в діапазоні 110.

#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), і це правило заощаджує багато часу й нервів.

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