JavaRush /Курсы /C++ SELF /Breakpoints — обычные и условные

Breakpoints — обычные и условные

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

1. Как работают breakpoints

Когда программа ведёт себя неправильно, хочется “поставить std::cout << "Я тут!" везде” — это естественная реакция человека, который впервые встретил баг. Проблема в том, что через 15 минут таких вставок вы получите «ёлку из принтов», удалять которую больнее, чем лечить зубы. Breakpoint — более аккуратный способ: вы останавливаете программу в нужной точке и смотрите реальное состояние, не переписывая код и не засоряя вывод.

Если упростить, breakpoint — это как поставить программу “на паузу” в конкретной строке, чтобы спокойно осмотреться: какие значения у переменных, как мы сюда пришли, и почему здравый смысл уже покинул чат.

Обычный breakpoint и момент срабатывания

Важно понимать одну тонкость, из-за которой новички иногда делают неверные выводы: breakpoint срабатывает до выполнения строки, на которой он стоит. То есть отладчик говорит: “Сейчас мы собираемся выполнить вот эту строку. Останавливаюсь, чтобы ты посмотрел, что было до изменения”.

Это удобно, потому что вы можете сравнить состояние “до” и “после”. Например, если строка увеличивает счётчик, то на breakpoint’е вы увидите старое значение, а после шага — новое (шаги подробно будут в следующей лекции, а сегодня мы просто держим в голове эту идею).

Мини-пример, просто чтобы почувствовать момент остановки:

#include <iostream>

int main() {
    int x = 10;
    x = x + 5;                 // breakpoint здесь: x ещё 10
    std::cout << x << '\n';    // вывод: 15
}

Если поставить breakpoint на строку x = x + 5;, то при остановке x ещё будет равен 10. После выполнения строки станет 15.

Где ставить breakpoints: логические узлы кода

Есть соблазн ставить breakpoint “куда попало” — и потом удивляться, что вы остановились 200 раз и всё равно ничего не поняли. Хорошая привычка: ставить точки останова туда, где принимаются решения или меняется состояние.

Обычно это места, где код “поворачивает” (условия), “повторяется” (циклы) или “фиксирует результат” (возвраты из функции). Не нужно превращать это в религию — это скорее как дорожные знаки: ставим там, где реально есть развилка или опасный поворот.

Ниже — небольшая шпаргалка в виде таблицы. Её не надо заучивать, но полезно пару раз на неё взглянуть перед реальной отладкой.

Место в коде Почему удобно Типичный вопрос
Вход в функцию Сразу видны аргументы “Мне вообще передали то, что я ожидал?”
Перед if Видно условие и значения “Почему мы пошли в эту ветку?”
Внутри цикла у изменения переменной Можно поймать момент поломки “На какой итерации всё стало странным?”
Перед return Проверяем итог “Почему вернули именно это?”

Управление breakpoints: поставить, отключить, удалить

В любой IDE/отладчике (не важно, это CLion, Visual Studio, VS Code, Qt Creator — принцип один) у breakpoint’а есть как минимум три “режима жизни”.

Сначала вы ставите breakpoint — обычно кликом слева от строки (в “гуттере”, возле номеров строк). Потом вы можете его удалить, если он больше не нужен. И есть третий вариант, очень недооценённый новичками: breakpoint можно временно отключить.

Отключение — это когда точка остаётся “в списке”, но программа на ней не останавливается. Это полезно, когда вы хотите быстро “приглушить шум”, но не хотите потом вспоминать: “А где я ставил ту важную точку, которая была почти правильная?”

Удобная ментальная модель такая: удаление — это “стереть отметку на карте”, а отключение — это “закрыть её шторкой”.

Ещё один важный практический момент: если breakpoint не сработал, это не обязательно “сломался отладчик”. Чаще всего причина проще и обиднее: до этой строки просто не дошло выполнение. Например, сработал ранний return, условие if не пустило, или вы отлаживаете не ту конфигурацию/не тот запуск.

Условные breakpoints

Условный breakpoint — это тот же breakpoint, но с фильтром: “останавливайся только если условие истинно”. Это спасение для циклов, больших данных и ситуаций “ошибка проявляется на 15342-й итерации, а я не хочу нажимать Continue 15341 раз”.

Но есть важная дисциплина: условие должно быть простым и безопасным. Идеальная проверка — сравнение индекса, проверка на конкретное значение, проверка на nullptr (когда вы дойдёте до указателей), проверка границ. Плохая идея — писать условие, которое меняет состояние программы. Отладка не должна превращаться в “квест: угадай, что сломал отладчик”.

Табличка для ориентира:

Тип breakpoint Когда использовать Пример условия
Обычный Хотим остановиться всегда в этом месте
Условный Хотим остановиться только в редкий момент
i == 100, sum < 0, v[i] == target

3. Практика: ловим баги на PocketBudget

Чтобы не отлаживать “сферического коня в вакууме”, давайте продолжим наш учебный стиль: у нас есть маленькое консольное приложение PocketBudget, которое работает со списком расходов/доходов (обычные int в std::vector<int>). Никакой магии: числа, циклы, функции — всё уже знакомо.

Ситуация: сумма считается неправильно

Предположим, мы написали функцию суммы, но где-то ошиблись. Вот минимальная версия:

#include <cstddef>
#include <vector>

int total_sum(const std::vector<int>& a) {
    int s = 0;
    for (std::size_t i = 1; i < a.size(); ++i) { // BUG: стартуем с 1
        s += a[i];
    }
    return s;
}

Она почти правильная… кроме того, что начинается с i = 1, то есть пропускает нулевой элемент. Это типичный баг уровня “я просто не заметил”.

Теперь main, который использует эту функцию:

#include <iostream>
#include <vector>

int total_sum(const std::vector<int>& a);

int main() {
    std::vector<int> ops{10, -3, -4};
    std::cout << total_sum(ops) << '\n'; // ожидаем 3, а получаем -7
}

Как здесь помогают breakpoints?

Вы ставите обычный breakpoint на строку s += a[i];. Запускаете отладку. Когда остановится — смотрите: чему равны i, s, a.size(), a[i]. Уже на первой остановке станет видно, что i == 1, а значит первый элемент a[0] вообще не учитывается.

А если вы не хотите “останавливаться много раз” (вдруг вектор большой), вы ставите условный breakpoint на той же строке, но с условием i == 1. Тогда остановка будет ровно одна: в первой итерации. И вы быстро проверите гипотезу: “мы правда стартуем не с того места”.

Ситуация: хотим поймать редкую итерацию в большом цикле

Иногда баг не такой очевидный: он проявляется на больших данных или только на конкретном шаге. Для демонстрации возьмём синтетический пример:

#include <iostream>

int main() {
    long long s = 0;
    for (int i = 0; i < 1'000'000; ++i) {
        s += i; // условный breakpoint: i == 999'999
    }
    std::cout << s << '\n';
}

Если поставить обычный breakpoint на s += i;, вы будете останавливаться миллион раз (и начнёте ненавидеть компьютеры). А условный breakpoint i == 999'999 даст вам остановку ровно один раз — на последней итерации, где можно проверить, какое финальное состояние “прямо перед завершением” цикла.

Ситуация: поиск элемента и два breakpoint’а на два исхода

Теперь пример ближе к реальным задачам: ищем первую операцию, которая превышает лимит (например, “найди первую трату больше 100”).

#include <cstddef>
#include <vector>

int first_over_limit(const std::vector<int>& ops, int limit) {
    for (std::size_t i = 0; i < ops.size(); ++i) {
        if (ops[i] > limit) return static_cast<int>(i); // breakpoint A
    }
    return -1; // breakpoint B
}

И main:

#include <iostream>
#include <vector>

int first_over_limit(const std::vector<int>& ops, int limit);

int main() {
    std::vector<int> ops{10, 50, 120, 30};
    std::cout << first_over_limit(ops, 100) << '\n'; // 2
}

Здесь удобно поставить два breakpoint’а: один на “успешный return”, другой на return -1;. Тогда при каждом запуске вы мгновенно отвечаете на вопрос: “мы нашли элемент или нет?”.

Если нашли — видите i, ops[i], limit. Если не нашли — значит цикл прошёл до конца, и нужно разбираться, почему условие ни разу не стало истинным (может, лимит не тот, может, данные не те, может, сравнение перепутано).

Очень практичная техника: breakpoint перед if

Иногда вы уверены, что условие должно сработать, а оно не срабатывает. Это классика. Тогда breakpoint прямо перед if — ваш лучший друг:

#include <iostream>

int main() {
    int balance = -5;
    if (balance > 0) {                 // breakpoint: balance действительно > 0?
        std::cout << "OK\n";
    } else {
        std::cout << "Not OK\n";        // Not OK
    }
}

На breakpoint’е вы смотрите: какое значение у balance? Возможно, вы думали, что оно положительное, а оно уже давно отрицательное — и проблема вообще не в if, а в том, кто присвоил баланс раньше. Breakpoint помогает поймать момент, когда ваша “картина мира” расходится с реальностью.

5. Типичные ошибки при работе с breakpoints

Ошибка №1: ставить breakpoint в строку, которая на самом деле не выполняется.
Новички иногда ставят точку останова на строку с фигурной скобкой, на пустую строку, на комментарий или на объявление переменной, которое оптимизатор/компилятор “свернул” так, что фактического исполняемого действия там нет. В результате кажется, что “breakpoint сломан”. Лечится просто: ставьте breakpoint на строку, где точно есть действие — присваивание, вызов функции, return, тело if или реальная строка внутри цикла.

Ошибка №2: ожидать, что breakpoint покажет уже изменённое значение.
Поскольку breakpoint срабатывает до выполнения строки, вы часто видите “старое” значение переменной. Это не баг — это нормальная логика. Если нужно увидеть “после изменения”, либо ставьте breakpoint на следующую строку, либо выполняйте шаг (но шаги — это следующая лекция).

Ошибка №3: поставить breakpoint слишком рано и утонуть в остановках.
Если вы ставите breakpoint в начале цикла на 100000 итераций, вы фактически устраиваете себе тренировку по нажатию кнопки Continue. Гораздо эффективнее поставить breakpoint ближе к месту, где проявляется симптом, или сделать его условным по индексу/значению.

Ошибка №4: писать в условии breakpoint’а что-то умное, но опасное.
Соблазн велик: “а давай в условии я вызову функцию, которая всё проверит”. Но условие должно быть максимально безопасным и не менять программу. Иначе вы рискуете отлаживать не программу, а последствия своего же условия. Лучше ограничиться проверками вида i == ..., x < 0, s.size() == ..., v[i] == target (только когда вы уверены, что i в границах).

Ошибка №5: остановились и сразу сделали вывод, не проверив контекст.
Breakpoint сам по себе не объясняет причину бага. Он лишь фиксирует момент времени. Важно при остановке задать себе правильный вопрос: “Какие значения должны быть истинны в этот момент?” и проверить их. Если просто смотреть на переменные без гипотезы, можно долго созерцать числа и чувствовать философскую пустоту.

1
Задача
C++ SELF, 34 уровень, 1 лекция
Недоступна
Два стоп‑кадра
Два стоп‑кадра
1
Задача
C++ SELF, 34 уровень, 1 лекция
Недоступна
Редкая итерация
Редкая итерация
1
Задача
C++ SELF, 34 уровень, 1 лекция
Недоступна
Пропущенный элемент
Пропущенный элемент
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ