JavaRush /Курсы /C++ SELF /Типовые ошибки: = vs ==, проверки ввода

Типовые ошибки: = vs ==, проверки ввода

C++ SELF
3 уровень , 5 лекция
Открыта

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 { ... }. Это добавляет пару строк, но экономит часы удивления.

1
Задача
C++ SELF, 3 уровень, 5 лекция
Недоступна
Индикатор нуля
Индикатор нуля
1
Задача
C++ SELF, 3 уровень, 5 лекция
Недоступна
Метка значения
Метка значения
1
Задача
C++ SELF, 3 уровень, 5 лекция
Недоступна
Сумма с проверкой
Сумма с проверкой
1
Задача
C++ SELF, 3 уровень, 5 лекция
Недоступна
Три команды
Три команды
1
Опрос
Условные операторы и логика, 3 уровень, 5 лекция
Недоступен
Условные операторы и логика
Условные операторы и логика
Комментарии (3)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Сергей Уровень 22
22 июня 2026
А почему if (std::cin >> dividend >> divisor) не проверяет на пустоту: если ввести не int, то выскакивает "input error", если вообще ничего не вводить, программа уходит в бесконечный цикл?
Anonymous #2923722 Уровень 17
4 мая 2026
#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'; } } В строках всегда будет срабатывать else из-за использования выше std::ws. Какой смысл проверять на размер? if (s.size() == 0) std::cout << "empty\n"; else std::cout << s[0] << '\n';
Юлия Уровень 6
20 апреля 2026
Судя по всему, в задаче "Сумма с проверкой" не учитывается, что пользователь может ввести проверочный код не напрямую в "if", а в предложенную переменную "const bool". И если решать задачу именно с использованием проверки через "const bool" - то все условия задания отмечаются галочками (код верный), но при этом в консоли ниже красным выводится какая-то ошибка синтаксиса, хотя ее нет.