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 | Когда использовать | Пример условия |
|---|---|---|
| Обычный | Хотим остановиться всегда в этом месте | — |
| Условный | Хотим остановиться только в редкий момент | |
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 сам по себе не объясняет причину бага. Он лишь фиксирует момент времени. Важно при остановке задать себе правильный вопрос: “Какие значения должны быть истинны в этот момент?” и проверить их. Если просто смотреть на переменные без гипотезы, можно долго созерцать числа и чувствовать философскую пустоту.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ