1. Ошибка века: = вместо ==
Выглядит как мелочь: один знак равенства или два. Но по смыслу это два совершенно разных действия. == — это вопрос «равно ли?», а = — это команда «положи вот это значение в переменную».
И коварство в том, что присваивание (=) тоже является выражением: оно возвращает присвоенное значение. Поэтому компилятор часто не обязан ругаться: он видит условие, которое вычисляется в число, а число в условии трактуется как «0 — ложь, не 0 — истина».
Представьте, что вы на экзамене вместо «Это правда?» спрашиваете «Сделай это правдой!». Преподаватель удивится, но формально вы произнесли слова русского языка. Вот = в if — примерно такой же трюк.
Мини-демо: как ошибка ломает и данные, и логику
Ниже пример, который компилируется, но делает не то, что хочется:
#include <iostream>
int main() {
int x = 5;
if (x = 0) { // ОШИБКА: присваивание вместо сравнения
std::cout << "x is zero\n";
}
std::cout << x << '\n'; // 0
}
Смысл происходящего: x = 0 присваивает ноль, выражение имеет значение 0, условие ложно, ветка if не выполняется, но x уже испорчен.
Правильный вариант
#include <iostream>
int main() {
int x = 5;
if (x == 0) { // сравнение
std::cout << "x is zero\n";
} else {
std::cout << "x is not zero\n"; // x is not zero
}
}
Почему компилятор не всегда спасает
Потому что с точки зрения языка это допустимо: условие if (...) должно вычислиться во что-то, приводимое к bool. Число к bool приводится. Значит — формально корректно.
Да, многие компиляторы предупреждают (warning), но предупреждение — это не гарантия, особенно в учебных Web‑IDE, где настройки могут быть мягкими. Поэтому ваша страховка — внимание к оператору в середине условия.
Привычка: сравниваем явно, без «намёков»
Плохая идея для новичка — писать «хитро»:
if (x) { /* ... */ } // работает, но хуже читается на старте
Хорошая идея — писать «в лоб»:
if (x != 0) { /* ... */ }
В учебном коде это почти всегда выигрывает читаемостью. А читаемость — это минус половина багов.
2. Проверка ввода: if (std::cin >> x)
Когда вы читаете число через std::cin >> x, вы молча предполагаете, что пользователь введёт именно число. Но пользователь — существо творческое. Он может ввести qwe, может нажать Enter не там, может скопировать 10.5 вместо 10. И тогда std::cin «обидится»: чтение не удастся, переменная может остаться прежней, а последующие чтения начнут вести себя странно.
Пока мы не разбираем подробные состояния потока (fail, bad, clear, ignore) — это будет в других темах. Сегодня нам достаточно минимального шаблона: проверка самого факта чтения.
Самый простой и полезный паттерн
#include <iostream>
int main() {
int n = 0;
if (std::cin >> n) {
std::cout << "read: " << n << '\n'; // например: read: 42
} else {
std::cout << "input error\n"; // если ввели не число
}
}
В условии if (std::cin >> n) происходит две вещи: сначала попытка прочитать число, затем — проверка, получилось ли. Это компактно, и на вашем текущем уровне — идеально.
Мини‑пример: команда и число, без надежды «авось прочиталось»
Мы продолжим идею простого «консольного помощника», который читает команду и выполняет действие. Пока без циклов: один запуск — одна команда.
#include <iostream>
#include <string>
int main() {
std::string cmd;
std::cin >> cmd;
if (cmd == "square") {
int x = 0;
if (std::cin >> x) {
std::cout << x * x << '\n'; // например: 25
} else {
std::cout << "input error\n";
}
}
}
Обратите внимание: проверка ввода находится там, где она нужна — рядом с использованием числа. Мы не «надеемся», что оно прочиталось.
3. Граничные случаи: возраст, диапазоны и строки
Границы — это те значения, на которых программа ломается чаще всего: 0, 1, -1, «ровно 18», пустая строка, последний символ строки, деление на ноль. Новичок обычно тестирует «середину» (например, 20 лет), а ошибка прячется в «краях» (18 лет).
Поэтому полезно проговаривать условие словами и проверять, где именно стоит граница: входит ли она или нет.
Чтобы мозгу было легче, держите в голове простое правило: любой доступ по индексу и любое деление требуют защиты. И даже если вам «кажется», что там всегда будет корректно, практика покажет, что кажется всем.
> vs >=: «включая границу» — это отдельное решение
Сравните две проверки:
#include <iostream>
int main() {
int age = 18;
std::cout << (age > 18) << '\n'; // 0
std::cout << (age >= 18) << '\n'; // 1
}
Логика «совершеннолетний с 18» требует >=, а не >. Это не стиль, это смысл.
Чуть более жизненный фрагмент:
#include <iostream>
int main() {
int age = 0;
std::cin >> age;
if (age >= 18) {
std::cout << "adult\n";
} else {
std::cout << "minor\n";
}
}
Если вы ошибётесь знаком, программа будет неверной ровно на одном значении. И именно поэтому такие баги живут долго: их редко тестируют.
Диапазон «от L до R включительно»: пишем через &&
Если условие звучит как «x от 1 до 10», это почти всегда означает «и не меньше 1, и не больше 10».
#include <iostream>
int main() {
int x = 0;
std::cin >> x;
if (x >= 1 && x <= 10) {
std::cout << "in range\n";
} else {
std::cout << "out of range\n";
}
}
Граница — часть задачи. Не просто «написать if», а решить, что делать на 1 и 10.
Пустая строка: прежде чем брать s[0], убедись, что строка не пуста
Строка — это не гарантия, что внутри хоть один символ. Если вы прочитали строку через std::getline, пользователь мог просто нажать Enter.
#include <iostream>
#include <string>
int main() {
std::string s;
std::getline(std::cin, s);
if (s.size() == 0) {
std::cout << "empty\n";
} else {
std::cout << s[0] << '\n'; // печатаем первый символ
}
}
Здесь защита «пусто/не пусто» — это не занудство. Это то, что отделяет программу от выхода за границы.
Последний символ: s[s.size() - 1] требует ещё большей внимательности
Потому что если s.size() == 0, выражение s.size() - 1 превращается в «минус один», а индексы так не любят.
#include <iostream>
#include <string>
int main() {
std::string s;
std::getline(std::cin, s);
if (s.size() > 0) {
char last = s[s.size() - 1];
std::cout << last << '\n';
} else {
std::cout << "empty\n";
}
}
Обратите внимание: проверка здесь > 0, а не >= 0 (вторая бессмысленна). И да, size() на пустой строке равен 0.
4. Защитные проверки: деление и короткое замыкание
Деление на ноль — классическая проблема. И особенно обидно, что она часто появляется не потому, что вы хотели делить на ноль, а потому что вы не подумали, что пользователь может ввести 0. А пользователь может. Даже если вы просили «введите делитель» — он всё равно может.
Короткое замыкание в && — это тот редкий случай, когда «ленивость» оператора делает вашу программу безопаснее: если левая часть ложна, правая даже не вычисляется. Значит, если слева стоит проверка безопасности, справа можно ставить опасное действие.
Правильный порядок частей в &&
#include <iostream>
int main() {
int a = 0;
int b = 0;
std::cin >> a >> b;
if (b != 0 && (a / b > 2)) {
std::cout << "a/b > 2\n";
} else {
std::cout << "condition is false\n";
}
}
Если b == 0, левая часть b != 0 ложна, и деления не будет. Это и есть защитная проверка.
Неправильный порядок (опасно)
#include <iostream>
int main() {
int a = 0;
int b = 0;
std::cin >> a >> b;
if ((a / b > 2) && b != 0) { // ОПАСНО: деление уже произошло
std::cout << "a/b > 2\n";
}
}
Даже если вы «проверяете b», вы проверяете его слишком поздно. Это как надеть шлем после падения с велосипеда: шлем отличный, но тайминг подкачал.
5. Практический мини‑пример: одна команда — одно действие
Сейчас соберём всё в маленькое приложение, которое показывает стиль «надёжных условий». Мы не используем циклы и функции (кроме main), потому что их ещё не проходили. Но мы уже можем сделать программу, которая не падает от каждого неудачного ввода и не делает опасных операций без проверок.
Идея: пользователь вводит команду (divide, first, last), затем необходимые данные.
Каркас ветвления по команде
#include <iostream>
#include <string>
int main() {
std::string cmd;
std::cin >> cmd;
if (cmd == "divide") {
std::cout << "mode: divide\n";
} else if (cmd == "first") {
std::cout << "mode: first\n";
} else {
std::cout << "unknown command\n";
}
}
Это базовая «развилка»: команда — это строка, сравнение строк даёт bool, if/else if/else выбирает одну ветку.
Команда divide: проверяем ввод и защищаем деление
#include <iostream>
#include <string>
int main() {
std::string cmd;
std::cin >> cmd;
if (cmd == "divide") {
int a = 0, b = 0;
if (std::cin >> a >> b) {
if (b != 0) std::cout << (a / b) << '\n';
else std::cout << "division by zero\n";
} else {
std::cout << "input error\n";
}
}
}
Тут сразу две линии защиты: сначала мы убеждаемся, что числа вообще прочитались, потом убеждаемся, что делитель не равен нулю.
Команда first: читаем строку и проверяем пустоту
С first есть нюанс: после std::cin >> cmd в буфере может остаться перевод строки, и getline может прочитать пустую строку. Мы подробно говорили об этом в теме про getline, поэтому здесь просто применяем аккуратный приём: читать строку через std::getline(std::cin >> std::ws, s) — std::ws «съест» пробелы и переводы строк перед реальным текстом.
#include <iostream>
#include <string>
int main() {
std::string cmd;
std::cin >> cmd;
if (cmd == "first") {
std::string s;
std::getline(std::cin >> std::ws, s);
if (s.size() == 0) std::cout << "empty\n";
else std::cout << s[0] << '\n';
}
}
Да, это чуть «магически» выглядит из-за std::ws, но вы уже встречали проблему «пустой строки после >>», и это одно из стандартных решений.
Команда last: последний символ без выхода за границы
#include <iostream>
#include <string>
int main() {
std::string cmd;
std::cin >> cmd;
if (cmd == "last") {
std::string s;
std::getline(std::cin >> std::ws, s);
if (s.size() > 0) std::cout << s[s.size() - 1] << '\n';
else std::cout << "empty\n";
}
}
Смысл тот же: сначала проверка, потом индексирование.
Схема «как мыслить» про опасные места
Когда вы пишете ветку if, полезно мысленно пройти такой маршрут:
flowchart TD
A[Нужно выполнить действие] --> B{Нужны данные?}
B -->|Да| C{Ввод успешен?}
C -->|Нет| X[Сообщение об ошибке]
C -->|Да| D{Есть опасная операция?}
D -->|Деление/индекс| E{Защита выполнена?}
E -->|Нет| Y[Сообщение + не делаем действие]
E -->|Да| F[Делаем действие]
B -->|Нет| F
Эта блок‑схема — не «формализм», а просто способ не забывать о проверках, особенно когда код начинает ветвиться.
6. Типичные ошибки
Ошибка №1: = вместо == в условии.
Это одна из самых неприятных ошибок, потому что она часто не мешает компиляции. = меняет переменную и возвращает присвоенное значение, поэтому условие может стать «ложным» или «истинным» не из-за сравнения, а из-за того, что вы только что что-то перезаписали. Лечится привычкой внимательно смотреть на «центральный оператор» условия и писать сравнение явно: x == 0, cmd == "exit".
Ошибка №2: перепутанная граница (> вместо >=, < вместо <=).
Такие ошибки почти всегда проявляются «ровно на одном значении»: 18, 0, «последний индекс». Поэтому они живут долго: вы тестируете 20, 30, 40 — всё работает. Привычка, которая помогает, звучит по‑детски, но работает железно: сначала сказать условие словами («включая 18»), потом выбрать знак.
Ошибка №3: индексирование строки без проверки длины.
std::string не обязан быть непустым. Если вы делаете s[0] или s[s.size() - 1], сначала должно быть условие s.size() > 0. Особенно коварен «последний символ», потому что выражение s.size() - 1 выглядит безобидно, но на пустой строке превращается в неправильный индекс. Защита — отдельная ветка if или левый аргумент в &&.
Ошибка №4: деление без проверки нуля (или проверка стоит «справа»).
Проверка b != 0 должна стоять до деления. Самый удобный паттерн — b != 0 && (a / b > ...), где короткое замыкание гарантирует, что деление не произойдёт при b == 0. Если поменять местами части условия, вы получите опасное поведение даже при наличии «проверки».
Ошибка №5: отсутствие минимальной проверки ввода.
Если вы делаете std::cin >> x; и сразу используете x, вы предполагаете корректный ввод. На практике это приводит к странным «залипаниям» и некорректным результатам, когда пользователь вводит не число. Даже без углубления в устройство потоков уже можно писать надёжнее: if (std::cin >> x) { ... } else { ... }. Это добавляет пару строк, но экономит часы удивления.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ