JavaRush /Курси /C++ SELF /Точки зупину — звичайні й умовні

Точки зупину — звичайні й умовні

C++ SELF
Рівень 34 , Лекція 1
Відкрита

1. Як працюють точки зупину

Коли програма поводиться неправильно, хочеться «поставити std::cout << "Я тут!" всюди» — це природна реакція людини, яка вперше натрапила на баг. Проблема в тому, що за 15 хвилин таких вставок ви отримаєте «ялинку з принтів», і видаляти її болючіше, ніж лікувати зуби. Точка зупину (breakpoint) — акуратніший спосіб: ви зупиняєте програму в потрібному місці й дивитеся на її реальний стан, не переписуючи код і не засмічуючи вивід.

Якщо спростити, точка зупину — це ніби поставити програму «на паузу» на конкретному рядку, щоб спокійно озирнутися: які значення мають змінні, як ми сюди дійшли і чому здоровий глузд уже покинув чат.

Звичайна точка зупину і момент спрацювання

Важливо розуміти одну тонкість, через яку новачки іноді роблять хибні висновки: точка зупину спрацьовує до виконання рядка, на якому вона стоїть. Тобто відладчик ніби каже: «Зараз ми збираємося виконати ось цей рядок. Зупиняюся, щоб ви побачили, що було до зміни».

Це зручно, бо ви можете порівняти стан «до» і «після». Наприклад, якщо рядок збільшує лічильник, то в точці зупину ви побачите старе значення, а після кроку — нове (про кроки детально поговоримо в наступній лекції, а сьогодні просто тримайте в голові цю ідею).

Міні-приклад — просто щоб відчути момент зупинки:

#include <iostream>

int main() {
    int x = 10;
    x = x + 5;                 // breakpoint тут: x ще 10
    std::cout << x << '\n';    // вивід: 15
}

Якщо поставити точку зупину на рядок x = x + 5;, то під час зупинки x ще дорівнюватиме 10. Після виконання рядка значення стане 15.

Де ставити точки зупину: логічні вузли коду

Є спокуса ставити точку зупину де завгодно, а потім дивуватися, що ви зупинилися 200 разів і все одно нічого не зрозуміли. Добра звичка — ставити точки зупину в тих місцях, де ухвалюються рішення або змінюється стан.

Зазвичай це місця, де код «розгалужується» (умови), «повторюється» (цикли) або «фіксує результат» (повернення з функції). Не варто перетворювати це на релігію — це радше дорожні знаки: ставимо там, де справді є розвилка або небезпечний поворот.

Нижче — невелика шпаргалка у вигляді таблиці. Її не треба вивчати напамʼять, але корисно кілька разів на неї поглянути перед реальною відладкою.

Місце в коді Чому це зручно Типове запитання
Вхід у функцію Одразу видно аргументи «Мені взагалі передали те, чого я очікував?»
Перед if Видно умову та значення «Чому ми пішли в цю гілку?»
Усередині циклу біля зміни змінної Можна спіймати момент поломки «На якій ітерації все стало дивним?»
Перед return Перевіряємо підсумок «Чому повернули саме це?»

Керування точками зупину: поставити, вимкнути, видалити

У будь-якій IDE чи відладчику (неважливо, це CLion, Visual Studio, VS Code, Qt Creator — принцип один) у точки зупину є щонайменше три «режими життя».

Спочатку ви ставите точку зупину — зазвичай клацанням ліворуч від рядка (у «gutter», біля номерів рядків). Потім можете її видалити, якщо вона більше не потрібна. Є і третій варіант, який новачки часто недооцінюють: точку зупину можна тимчасово вимкнути.

Вимкнення означає, що точка залишається «у списку», але програма на ній не зупиняється. Це корисно, коли ви хочете швидко «приглушити шум», але не хочете потім згадувати: «А де саме була та важлива точка, яка майже спрацювала як треба?»

Зручна ментальна модель проста: видалення — це «стерти позначку на мапі», а вимкнення — це «закрити її шторкою».

Ще один важливий практичний момент: якщо точка зупину не спрацювала, це не означає, що «зламався відладчик». Найчастіше причина простіша й прикріша: до цього рядка просто не дійшло виконання. Наприклад, спрацював ранній return, умова if не пропустила, або ви відладжуєте не ту конфігурацію чи не той режим запуску.

Умовні точки зупину

Умовна точка зупину — це та сама точка зупину, але з фільтром: «зупиняйся, тільки якщо умова істинна». Це порятунок для циклів, великих даних і ситуацій на кшталт «помилка проявляється на 15 342-й ітерації, а я не хочу натискати Continue 15 341 раз».

Але тут є важливе правило: умова має бути простою й безпечною. Ідеальний варіант — порівняння індексу, перевірка на конкретне значення, перевірка на nullptr (коли ви дійдете до вказівників), перевірка меж. Погана ідея — писати умову, яка змінює стан програми. Відладка не повинна перетворюватися на «квест: вгадай, що зламав відладчик».

Невелика таблиця для орієнтиру:

Тип точки зупину Коли використовувати Приклад умови
Звичайна Коли хочемо зупинятися в цьому місці завжди
Умовна Коли хочемо зупинитися лише в рідкісний момент
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
}

Як тут допоможуть точки зупину?

Ви ставите звичайну точку зупину на рядок s += a[i];. Запускаєте відладку. Коли програма зупиниться, дивитеся, чому дорівнюють i, s, a.size(), a[i]. Уже на першій зупинці стане видно, що i == 1, а отже перший елемент a[0] взагалі не враховується.

А якщо ви не хочете багато разів зупинятися (наприклад, коли вектор великий), поставте умовну точку зупину на тому самому рядку, але з умовою 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';
}

Якщо поставити звичайну точку зупину на s += i;, ви зупинятиметеся мільйон разів (і почнете ненавидіти компʼютери). А умовна точка зупину i == 999'999 дасть вам зупинку рівно один раз — на останній ітерації, де можна перевірити, який фінальний стан «прямо перед завершенням» циклу.

Ситуація: пошук елемента і дві точки зупину для двох результатів

Тепер приклад ближче до реальних задач: шукаємо першу операцію, яка перевищує ліміт (наприклад, «знайди першу витрату, більшу за 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); // точка зупину A
    }
    return -1; // точка зупину 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
}

Тут зручно поставити дві точки зупину: одну на «успішний return», другу — на return -1;. Тоді під час кожного запуску ви миттєво відповідаєте на запитання: «Ми знайшли елемент чи ні?».

Якщо знайшли — бачите i, ops[i], limit. Якщо не знайшли — це означає, що цикл пройшов до кінця, і треба розбиратися, чому умова жодного разу не спрацювала (може, ліміт не той, може, дані не ті, а може, порівняння переплутано).

Дуже практична техніка: точка зупину перед if

Іноді ви впевнені, що умова має спрацювати, а вона не спрацьовує. Це класика. Тоді точка зупину прямо перед 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
    }
}

У точці зупину ви дивитеся: яке значення має balance? Можливо, ви думали, що воно додатне, а воно вже давно відʼємне — і проблема взагалі не в if, а в тому, де баланс отримав це значення раніше. Точка зупину допомагає спіймати момент, коли ваша «картина світу» розходиться з реальністю.

5. Типові помилки під час роботи з точками зупину

Помилка № 1: ставити точку зупину на рядок, який насправді не виконується.
Новачки інколи ставлять точку зупину на рядок із фігурною дужкою, на порожній рядок, на коментар або на оголошення змінної, яке оптимізатор або компілятор «згорнув» так, що фактичної виконуваної дії там немає. У результаті здається, що «точка зупину зламана». Лікується просто: ставте точку зупину на рядок, де точно є дія — присвоювання, виклик функції, return, тіло if або реальний рядок усередині циклу.

Помилка № 2: очікувати, що точка зупину покаже вже змінене значення.
Оскільки точка зупину спрацьовує до виконання рядка, ви часто бачите «старе» значення змінної. Це не баг — це нормальна логіка. Якщо потрібно побачити значення «після зміни», або ставте точку зупину на наступний рядок, або виконуйте крок (але кроки — це тема наступної лекції).

Помилка № 3: поставити точку зупину занадто рано й потонути в зупинках.
Якщо ви ставите точку зупину на початку циклу на 100 000 ітерацій, то фактично влаштовуєте собі тренування з натискання кнопки Continue. Значно ефективніше поставити точку зупину ближче до місця, де проявляється симптом, або зробити її умовною за індексом чи значенням.

Помилка № 4: писати в умові точки зупину щось розумне, але небезпечне.
Спокуса велика: «А давайте я в умові викличу функцію, яка все перевірить». Але умова має бути максимально безпечною й не змінювати програму. Інакше ви ризикуєте відлагоджувати не програму, а наслідки власної ж умови. Краще обмежитися перевірками на кшталт i == ..., x < 0, s.size() == ..., v[i] == target (тільки коли ви впевнені, що i у межах).

Помилка № 5: зупинилися й одразу зробили висновок, не перевіривши контекст.
Точка зупину сама по собі не пояснює причину бага. Вона лише фіксує момент у часі. Під час зупинки важливо поставити собі правильне запитання: «Які значення я очікую побачити в цей момент?» — і перевірити це. Якщо просто дивитися на змінні без гіпотези, можна довго споглядати числа й відчувати філософську порожнечу.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ