Типичные ошибки начинающих

Восемь ошибок, которые допускает почти каждый, кто учится программировать. Разбираем каждую: как выглядит плохой код, что именно не так, как правильно. В конце — интерактивный тренажёр: попробуйте сами найти ошибку в чужой программе.

Отладка Разбор кода Тренажёр
Важно понять с самого начала. Ошибки — это не признак того, что «программирование не для вас». Ошибки — это рабочий материал. Опытный программист отличается от новичка не тем, что не ошибается, а тем, что быстро узнаёт знакомые ошибки и знает, где их искать. Именно этому и посвящена страница: соберите в голове каталог типичных промахов, и в следующий раз вы их узнаете почти мгновенно.

Восемь классических ошибок

Кликните по карточке, чтобы развернуть полный разбор.

1

Целочисленное деление

›
Плохой код
int a = 7;
int b = 2;
double result = a / b;

cout << result;   // ожидаем 3.5, получаем 3
Что произошло. Операция / работает по-разному для целых и вещественных чисел. Если оба операнда целые, деление целочисленное — дробная часть отбрасывается. 7 / 2 даёт 3, и только после этого результат «приводится» к double — уже как 3.0. Тип результата определяется до присваивания.
Правильно
int a = 7;
int b = 2;
double result = (double)a / b;   // 3.5

// или так:
double result2 = a / 2.0;        // 3.5 — делитель вещественный
Правило. Хотите дробный результат — сделайте хотя бы один операнд вещественным явно: приведением типа (double) или записав делитель как 2.0. Особенно коварно это в формулах: (a + b + c) / 3 для среднего арифметического даст целое число.
2

Забытая инициализация накопителя

›
Плохой код
int sum;                // мусор в памяти
for (int i = 1; i <= 5; i++)
    sum += i;

cout << sum;            // каждый запуск разное!
Что произошло. Переменная sum объявлена, но не инициализирована — в ней лежит «мусор» от предыдущих программ. Прибавляя к нему числа, мы каждый раз получаем разный результат. Ошибка опасна тем, что программа не падает — просто считает неправильно, иногда незаметно.
Правильно
int sum = 0;            // накопитель — обнуляем
for (int i = 1; i <= 5; i++)
    sum += i;

cout << sum;            // 15
Правило. Перед использованием переменной-накопителя всегда задумайтесь, с какого значения начинается накопление. Для суммы это 0, для произведения — 1, для поиска максимума — первый элемент массива или очень маленькое число.
3

= вместо ==

›
Плохой код
int x = 5;
if (x = 3) {           // присваивание, а не сравнение!
    cout << "x равно 3";
}
cout << "x = " << x;   // x стало 3, а не 5
Что произошло. Один знак = — присваивание (положи в переменную значение). Два знака == — сравнение (равно ли одно другому). В условии if (x = 3) программа присваивает x значение 3, и это присваивание возвращает 3 — оно ненулевое, значит, условие истинно. Программа молча делает не то, что вы хотели.
Правильно
int x = 5;
if (x == 3) {          // сравнение
    cout << "x равно 3";
}
cout << "x = " << x;   // x = 5, как и было
Компилятор предупредит. Включите флаги -Wall -Wextra в g++ — GCC выдаст предупреждение «suggest parentheses around assignment used as truth value». Это сигнал: вы почти наверняка опечатались.
4

Лишняя точка с запятой после if или for

›
Плохой код
int sum = 0;
for (int i = 1; i <= 5; i++);   // ← вот тут ;
{
    sum += i;                    // эта строка выполнится один раз
}

cout << sum;                     // ошибка компиляции или мусор
Что произошло. Точка с запятой завершает оператор. Записав for (...);, вы говорите: «тело цикла пустое». Цикл честно отработает 5 раз, ничего не делая. А фигурные скобки со sum += i; — это уже отдельный блок, который выполнится один раз, да ещё и после выхода из цикла, где переменная i недоступна.
Правильно
int sum = 0;
for (int i = 1; i <= 5; i++) {
    sum += i;
}

cout << sum;                    // 15
Особенно коварно для if: if (x > 0); y = 1; — присваивание y = 1 выполнится всегда, независимо от условия. Найти такую ошибку взглядом очень трудно.
5

Выход за границы массива

›
Плохой код
int a[5] = {10, 20, 30, 40, 50};

// пытаемся обойти все 5 элементов
for (int i = 1; i <= 5; i++) {
    cout << a[i] << " ";   // при i=5 — выход за границу!
}
Что произошло. В C++ индексы массива начинаются с 0. Массив из 5 элементов имеет индексы 0, 1, 2, 3, 4. Обращение к a[5] читает шестой элемент — который находится за пределами выделенной памяти. Что там лежит — неизвестно: мусор, чужие данные, и читать их нельзя.
Правильно
int a[5] = {10, 20, 30, 40, 50};

for (int i = 0; i < 5; i++) {
    cout << a[i] << " ";   // i = 0, 1, 2, 3, 4
}
Почему не падает сразу. C++ не проверяет границы массивов во время выполнения. Программа может работать «нормально», но на самом деле читать или портить чужие данные. Это источник самых неприятных, трудно находимых багов.
6

Отсутствие фигурных скобок в if

›
Плохой код
if (x > 0)
    cout << "положительное";
    cout << "число";       // выполняется всегда!
Что произошло. Без фигурных скобок if управляет только одной следующей строкой. Первый cout действительно внутри условия. Второй — уже снаружи, он выполняется независимо от того, положительное ли x. Отступ в коде обманчив — он ничего не значит для компилятора.
Правильно
if (x > 0) {
    cout << "положительное";
    cout << "число";
}
Правило хорошего тона. Ставьте фигурные скобки всегда, даже если внутри одна строка. Так вы никогда не попадёте в эту ловушку, и код будет легче расширять.
7

Сравнение C-строк через ==

›
Плохой код
char name[20];
cin >> name;

if (name == "admin") {        // всегда ложь!
    cout << "Доступ разрешён";
}
Что произошло. Массив символов char[] — это адрес первого символа в памяти, а не строка в человеческом смысле. Оператор == сравнивает адреса, а не содержимое. Адрес массива name и адрес литерала "admin" — всегда разные, поэтому условие всегда ложно.
Правильно
char name[20];
cin >> name;

if (strcmp(name, "admin") == 0) {   // функция из <cstring>
    cout << "Доступ разрешён";
}

// Альтернатива — std::string
string name2;
cin >> name2;
if (name2 == "admin") {             // работает как ожидается
    cout << "Доступ разрешён";
}
Запомните. Для char[] используйте функции strcmp, strcpy, strcat. Либо переходите на std::string — там операторы ==, +, = работают интуитивно.
8

cin >> для строки с пробелами

›
Плохой код
char text[80];
cin >> text;

// Если пользователь ввёл "Hello, World!"
// в text попадёт только "Hello,"
Что произошло. Оператор cin >> читает до первого пробела (или табуляции, или перевода строки). Всё, что идёт после пробела, останется во входном потоке и попадёт в следующий cin — часто с неожиданным результатом.
Правильно
char text[80];
cin.getline(text, 80);       // прочитает всю строку целиком

// или для std::string:
string text2;
getline(cin, text2);

// или с явным ограничением символов:
cin.get(text, 80);
Осторожно после cin >>. Если до этого вы уже читали числа через cin >>, в буфере может остаться символ перевода строки. Тогда getline немедленно прочитает пустую строку. Обычное решение — перед getline вызвать cin.ignore().

Тренажёр: найдите ошибку

Пять фрагментов кода. В каждом ровно одна ошибка. Выберите правильный вариант ответа.

0 / 5
правильных ответов

Мини-чеклист на каждый запуск

Прежде чем жаловаться «не работает», проверьте по порядку:

Синтаксис

  • Все точки с запятой на месте — и их нет там, где не нужно?
  • Скобки { } и ( ) закрыты парно?
  • Названия переменных написаны одинаково (регистр!)?

Логика

  • Переменные инициализированы перед использованием?
  • Границы циклов: с чего начинается счётчик и чем заканчивается?
  • Деление: где-то не нужен ли (double)?
  • Сравнение строк: точно strcmp или std::string?

Данные

  • Массив не выходит за свои границы?
  • Ввод считывает то, что вы ожидаете (проверьте с явным выводом)?
  • Первый и последний элементы обрабатываются корректно?

Инструменты

  • Включены предупреждения компилятора (-Wall -Wextra)?
  • Пробовали запустить в отладчике с точкой останова?
  • Пробовали печатать промежуточные значения через cout?
Совет. Если программа ведёт себя странно, самый быстрый способ понять причину — вывести промежуточные значения на экран. Одна-две строки вида cout << "i=" << i << " sum=" << sum << endl; в подозрительном месте показывают, что происходит на самом деле.