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. Это не «болтовня», а способ сделать код самодокументируемым и снизить шанс ошибки в логике.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ