1. Вступ
Коли ви вперше бачите if (x > 0), здається, що цього досить: є перевірка — є рішення. Але в реальній задачі умова майже ніколи не зводиться до одного порівняння.
Зазвичай її можна сформулювати звичайною людською мовою, і в ній буде кілька вимог: «число має бути в діапазоні», «команда може називатися так або так», «ділити можна лише тоді, коли дільник не нуль», «символ можна брати лише тоді, коли рядок не порожній».
І саме тут порівняння перетворюються на цеглинки, а логічні операції — на цемент. Вони дають змогу скласти одну умову з кількох простіших. І зробити це так, щоб код не лише працював, а й не завершувався помилкою на рівному місці, наприклад через ділення на нуль. У цій лекції триматимемо в голові просту думку: логічні операції — це не «краса», а інструмент безпеки й здорового глузду.
Щоб приклади складалися в цілісну картину, розвиватимемо міні‑застосунок SmartCalc: програма читає команду ("add", "sub", "mul", "div", "mod") і два цілі числа, а потім або виводить результат, або повідомляє, чому дію не можна виконати. Цикли ми ще не проходили, тому програма виконує лише одну операцію і завершує роботу.
2. && — логічне І
Оператор && читається як «І». Він потрібен, коли мають одночасно виконуватися кілька умов. Простий життєвий приклад: «вхід дозволено, якщо у вас є квиток і вам уже 18».
Правило просте: вираз A && B істинний лише тоді, коли істинні обидва вирази: і A, і B. Якщо хоча б один із них хибний, хибним буде й увесь результат. При цьому && працює «ліниво» й іноді навіть не обчислює праву частину — це і називається коротким замиканням.
Таблиця істинності для &&
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Приклад 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, праву частину можна не обчислювати.
Таблиця істинності для ||
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Приклад 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. Це не «балаканина», а спосіб зробити код самодокументованим і зменшити ймовірність помилки в логіці.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ