JavaRush /Курси /C++ SELF /Логічні операції: &&, ||, !, коротке замикання

Логічні операції: &&, ||, !, коротке замикання

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

1. Вступ

Коли ви вперше бачите if (x > 0), здається, що цього досить: є перевірка — є рішення. Але в реальній задачі умова майже ніколи не зводиться до одного порівняння.

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

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

Щоб приклади складалися в цілісну картину, розвиватимемо міні‑застосунок SmartCalc: програма читає команду ("add", "sub", "mul", "div", "mod") і два цілі числа, а потім або виводить результат, або повідомляє, чому дію не можна виконати. Цикли ми ще не проходили, тому програма виконує лише одну операцію і завершує роботу.

2. && — логічне І

Оператор && читається як «І». Він потрібен, коли мають одночасно виконуватися кілька умов. Простий життєвий приклад: «вхід дозволено, якщо у вас є квиток і вам уже 18».

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

Таблиця істинності для &&

A
B
A && B
false
false
false
false
true
false
true
false
false
true
true
true

Приклад 1: перевірка діапазону

Перевірка «число від 1 до 10 включно» майже завжди виглядає так:

#include <iostream>

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

    if (x >= 1 && x <= 10) {
        std::cout << "ok\n";        // ok
    } else {
        std::cout << "not ok\n";    // not ok
    }
}

Тут x >= 1 — перша частина, а x <= 10 — друга. Для діапазону мають виконуватися обидві умови, інакше зміст «від… до…» втрачається.

Приклад 2: перевірка для SmartCalc

У нашому SmartCalc є операції "div" і "mod", для яких критично важливо, щоб дільник не був нулем. Поки що просто зафіксуємо ідею: перед «небезпечною операцією» нам потрібна умова на кшталт «команда — ділення і дільник не нуль».

#include <iostream>
#include <string>

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

    if (cmd == "div" && b != 0) {
        std::cout << (a / b) << '\n';   // безпечно
    }
}

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

3. || — логічне АБО

Оператор || читається як «АБО». Він потрібен, коли умова допускає кілька варіантів: «можете увійти, якщо у вас є перепустка або ви є у списку гостей».

Правило таке: A || B істинне, якщо істинний хоча б один із виразів. Якщо обидва хибні, результат теж хибний. Так само || підтримує коротке замикання: якщо ліворуч уже true, праву частину можна не обчислювати.

Таблиця істинності для ||

A
B
A || B
false
false
false
false
true
true
true
false
true
true
true
true

Приклад 1: кілька варіантів команди

Припустімо, ми хочемо, щоб користувач міг вводити і коротку форму команди, і довгу. Наприклад: "add" і "+".

#include <iostream>
#include <string>

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

    if (cmd == "add" || cmd == "+") {
        std::cout << "addition selected\n"; // addition selected
    }
}

Це і є випадок «один із варіантів». Без || довелося б писати два окремі if або вигадувати дивні конструкції.

Приклад 2: «так/ні» з кількома відповідями

Дуже поширений підхід — приймати "y" і "yes" як одну й ту саму відповідь.

#include <iostream>
#include <string>

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

    if (ans == "y" || ans == "yes") {
        std::cout << "confirmed\n";        // confirmed
    } else {
        std::cout << "not confirmed\n";    // not confirmed
    }
}

Важливий момент: логічне «АБО» — це не «додавання рядків» і не побітове АБО. Поки що ми працюємо лише з логікою: значеннями bool, отриманими з порівнянь.

4. ! — логічне НЕ

Оператор ! читається як «НЕ». Він інвертує логічне значення: true перетворює на false, а false — на true.

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

Приклад 1: «x не дорівнює нулю» через !

Можна написати так:

#include <iostream>

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

    bool ok = !(x == 0);
    std::cout << ok << '\n';   // 0, якщо x==0, інакше 1
}

Це не «краще» і не «гірше», ніж x != 0, але допомагає відчути механіку: x == 0 дає bool, а потім ! інвертує результат.

Приклад 2: заборона порожнього рядка перед індексуванням

Ми вже знаємо, що s[0] небезпечно, якщо рядок порожній. Умову «рядок НЕ порожній» можна записати так:

#include <iostream>
#include <string>

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

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

Так, можна написати і s.size() != 0. Але форма з ! корисна, коли ви хочете інвертувати не одне порівняння, а цілу складену умову. І саме там дужки вже стають обовʼязковими.

5. Коротке замикання

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

Для A && B правило таке: якщо A виявилося false, то весь результат точно false, і обчислювати B уже немає сенсу. Для A || B навпаки: якщо A виявилося true, то весь результат точно true, і обчислювати B уже не потрібно.

Ця поведінка — не просто «оптимізація», а частина мови: ви можете на неї розраховувати. Саме завдяки цьому ми й можемо писати захисні умови.

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

Міні-схема: як && ухвалює рішення

flowchart TD
    A["Обчислити A"] --> B{A == true?}
    B -- "ні" --> C["Результат false, B не обчислюємо"]
    B -- "так" --> D["Обчислити B"]
    D --> E["Результат = B"]

6. Захисні умови: ділення та індексування

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

Приклад 1: безпечне ділення через &&

Якщо написати так, буде погано:

// так робити НЕ треба
if (a / b > 2 && b != 0) { /* ... */ }

Бо ділення виконається раніше, ніж перевірка b != 0. Правильний порядок такий: спочатку перевірка, потім ділення. Коротке замикання гарантує, що ділення не відбудеться, якщо b == 0.

#include <iostream>

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

    if (b != 0 && (a / b > 2)) {
        std::cout << "a/b > 2\n";           // безпечно
    } else {
        std::cout << "no or unsafe\n";      // no or unsafe
    }
}

Тут, якщо b == 0, ліва частина b != 0 — хибна, і права частина (a / b > 2) навіть не обчислюватиметься. Це не магія, а та сама «лінивість», яка рятує програму від падіння.

Приклад 2: безпечний доступ до першого символу рядка

З std::string історія така сама: s[0] безпечний лише тоді, коли s.size() > 0. Тому перевірку ставимо ліворуч:

#include <iostream>
#include <string>

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

    if (s.size() > 0 && s[0] == '#') {
        std::cout << "comment line\n";  // comment line
    } else {
        std::cout << "not a comment\n"; // not a comment
    }
}

Якщо рядок порожній, s.size() > 0 дасть false, і вираз s[0] == '#' не обчислиться. Тобто ми «прикрили» потенційно небезпечний доступ простою перевіркою.

7. SmartCalc: умови з кількох перевірок

Час зібрати міні‑версію SmartCalc, у якій && і || працюють разом: || — щоб розпізнавати кілька варіантів команди, && — щоб додавати безпеку (дільник не нуль), а ! — щоб зручно обробляти невідповідні випадки.

Одразу домовмося про формат введення: користувач вводить команду і два числа. Наприклад: "div" 10 2. Програма виконує одну операцію і завершує роботу.

Крок 1: розпізнаємо команди синонімами (||)

#include <iostream>
#include <string>

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

    if (cmd == "add" || cmd == "+") {
        std::cout << (a + b) << '\n';   // наприклад: 7
    }
}

У такому стилі легко додати "sub"/"-", "mul"/"*". Поки що залишимо одну гілку, щоб приклад не розростався.

Крок 2: додаємо ділення із захистом (&& + коротке замикання)

#include <iostream>
#include <string>

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

    if (cmd == "div" || cmd == "/") {
        if (b != 0) {
            std::cout << (a / b) << '\n';       // 5, якщо ввести: div 10 2
        } else {
            std::cout << "division by zero\n";  // division by zero
        }
    }
}

Тут ми поки що зробили «двоповерховий» if: зовнішній обирає команду, а внутрішній перевіряє безпеку. Це нормально й зручно для читання.

Але інколи хочеться записати все «в один рядок», і тут якраз стане в пригоді &&:

#include <iostream>
#include <string>

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

    if ((cmd == "div" || cmd == "/") && b != 0) {
        std::cout << (a / b) << '\n';   // безпечно: ділимо лише якщо b!=0
    }
}

Така умова читається майже як звичайна українська фраза: «якщо команда "div" АБО "/", І водночас b не нуль — ділимо».

Крок 3: ! як «заперечення фільтра»

Іноді легше написати: «якщо команду не розпізнано — вивести помилку». Для цього створюємо булеву змінну, а потім використовуємо !. Це підвищує читабельність: умова отримує імʼя, а не перетворюється на кашу.

#include <iostream>
#include <string>

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

    bool known = (cmd == "add" || cmd == "+" || cmd == "div" || cmd == "/");
    if (!known) {
        std::cout << "unknown command\n";    // unknown command
    }
}

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

Як не перетворити умову на ребус: даємо перевіркам імена

Навіть коли ви вже вмієте користуватися && і ||, є небезпека написати одну гігантську умову, яка технічно правильна, але психологічно нечитабельна. Тому хороший прийом — виносити частини умови в bool-змінні з промовистими іменами.

Важливо, що це не «зайвий код», а спосіб задокументувати намір. Компілятор за потреби це оптимізує, а вам буде простіше читати.

Приклад: читабельна перевірка ділення в SmartCalc

#include <iostream>
#include <string>

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

    bool is_div = (cmd == "div" || cmd == "/");
    bool safe = (b != 0);

    if (is_div && safe) {
        std::cout << (a / b) << '\n';       // наприклад: 3
    } else if (is_div && !safe) {
        std::cout << "division by zero\n";  // division by zero
    }
}

Тут умова не стала «розумнішою», зате стала зрозумілішою: ви одразу бачите сенс, а не розгадуєте набір символів.

8. Типові помилки під час роботи з &&, ||, ! і коротким замиканням

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

Помилка №1: переплутати && і || у діапазонах.
Коли перевіряють «x від 1 до 10», новачки іноді пишуть x >= 1 || x <= 10. Така умова майже завжди буде істинною: будь-яке число або не менше за 1, або не більше за 10. Для діапазону потрібне саме «і»: x >= 1 && x <= 10, бо обидва обмеження мають виконуватися одночасно.

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

Помилка №3: очікувати, що обидві частини завжди обчислюються.
Іноді в правій частині && або || люди випадково ховають щось «важливе» — наприклад, дію, яку хотіли виконувати завжди. Але коротке замикання може не дати правій частині виконатися. Тому логічні оператори — не місце для дій «про всяк випадок». Нехай у A && B і A || B будуть саме перевірки, а дії — усередині тіла if.

Помилка №4: зловживати ! без дужок і заперечити не те.
Якщо ви пишете !x == 0, то це зазвичай не те, що ви хотіли, та й читається воно погано. Користуйтеся простим правилом: якщо ви хочете інвертувати порівняння, пишіть !(x == 0) або використовуйте прямий оператор !=. А якщо інвертуєте складену умову, дужки обовʼязкові, інакше дуже легко «перевернути» лише половину сенсу.

Помилка №5: написати умову-«макаронину» й самому ж у ній помилитися.
Коли умова стає довгою, зростає шанс переплутати дужки, забути частину перевірки або поставити не той оператор. Хороша звичка: винести частини умови в bool-змінні з іменами is_div, has_input, safe, in_range. Це не «балаканина», а спосіб зробити код самодокументованим і зменшити ймовірність помилки в логіці.

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