JavaRush /Курси /C++ SELF /Що робить дебагер: зупинка й покрокове виконання

Що робить дебагер: зупинка й покрокове виконання

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

1. Навіщо потрібен дебагер

Коли програма поводиться дивно, у новачка зазвичай є два інструменти: паніка та std::cout << "Я тут!\n";. Паніка не допомагає, а cout — річ корисна, але інколи він перетворює код на новорічну гірлянду з повідомлень. Дебагер дає змогу зупинити програму під час виконання й спокійно подивитися: «Що в мене у змінних? Яку гілку if насправді обрано? Чому цикл не завершується?».

Дебагер потрібен, тому що більшість багів — це не «компілятор зламався», а щось із трьох:

  1. програма виконується не так, як ви думаєте;
  2. дані всередині програми не такі, як ви думаєте;
  3. програма взагалі не виконується (чекає на введення, потрапила в нескінченний цикл, аварійно завершилася тощо).

Виведення через std::cout показує лише те, що ви вирішили надрукувати, і лише після того, як ви вставите цей код, перезберете проєкт, запустите програму й зловите потрібний момент. Дебагер показує стан прямо зараз. Ба більше, ви можете просувати виконання вперед крок за кроком.

Для ментальної моделі достатньо простої думки: «std::cout — це „я залишив записку самому собі“, а дебагер — це „я зупинив час і зазирнув у голову програмі“».

2. Що робить дебагер у налагоджувальній сесії

Уявіть, що ваша програма — це потяг, який їде рейками вихідного коду. Зазвичай ви натискаєте Run — і потяг пролітає від main() до кінця, а ви бачите лише кінцеві станції (виведення в консоль, результат, аварійне завершення).

Дебагер працює інакше: він запускає той самий потяг, але дає вам пульт керування. Ви можете:

  • зупинити потяг;
  • продовжити рух;
  • зробити один «крок» уперед;
  • подивитися на «вантаж» (значення змінних).

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

Ось проста схема того, що відбувається:

flowchart LR
    A["Вихідний код (.cpp)"] --> B["Компіляція (Debug)"]
    B --> C["Виконуваний файл + налагоджувальні символи"]
    C --> D["Запуск під дебагером"]
    D --> E["Зупинка / перегляд змінних / кроки"]

Термін сесія (debug session) означає: запуск програми під контролем дебагера. Тобто це той самий запуск, але з додатковими можливостями керування.

3. Що означає «програму зупинено»

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

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

Тобто ви дивитеся на точку в часі — ніби за мить до виконання.

Звідки береться зупинка

Сьогодні ми не занурюємося в тонкощі точок зупинки — це тема окремої лекції, — але загальну картину варто знати вже зараз. Програма може зупинитися, тому що:

  • ви вручну натиснули Pause (пауза);
  • ви запускаєте налагодження від початку, і IDE зупиняється на старті main() (деякі IDE так уміють);
  • програма дійшла до заздалегідь позначеного місця (зазвичай це breakpoint);
  • програма завершилася (дебагер просто повідомляє: «усе»);
  • сталося аварійне завершення (ділення на нуль, вихід за межі масиву тощо);
  • програма «зависла» — а насправді чекає на введення (це окремий і дуже поширений випадок).

Останній пункт особливо важливий для новачків: програма не зобовʼязана «бігти». Вона може цілком чесно стояти на рядку введення й чекати, доки ви введете число. І дебагер тут дуже допомагає: ви бачите точний рядок, на якому вона чекає.

4. Покрокове виконання

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

У різних IDE кнопки називаються трохи по-різному, але базова ідея одна: є команди, якими ви керуєте темпом виконання.

Нам сьогодні достатньо зрозуміти, що таке один крок:

  • виконати наступну «видиму» операцію;
  • знову зупинитися;
  • дати вам змогу подивитися значення.

Таблиця-орієнтир (без привʼязки до конкретної IDE):

Дія Що відбувається Як це відчувається
Continue / Resume Програма «біжить» далі Як звичайний запуск, але з можливістю знову зупинитися
Pause Програма зупиняється в поточному місці Ви буквально «заморозили» виконання
Stop Налагоджувальна сесія припиняється Програма завершується (принаймні для поточної сесії)
Step (один крок) Виконується наступна операція «Кадр уперед» у кіно, тільки для коду

Чому один рядок — не завжди один крок

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

Тому добра звичка — а вона окупається на 100 % — писати код так, щоб в одному рядку була одна зрозуміла дія, особливо якщо ви підозрюєте, що саме це місце доведеться налагоджувати.

Наприклад, ось так — важкувато:

int z = (x + y) * (a - b);

А ось так — дружніше і до дебагера, і до вас у майбутньому:

int sum = x + y;
int diff = a - b;
int z = sum * diff;

Так, рядків стало більше. Зате ви можете покроково перевірити кожну ідею: «sum справді такий? diff не відʼємний?».

Як мислити під час покрокового налагодження

Покрокове налагодження легко перетворити на беззмістовне «тик-тик-тик по кнопці Step», якщо не ставити собі правильного запитання. Тому тримайте в голові дуже практичну трійку.

Спочатку ви формулюєте запитання: «Чому cnt не зростає?» або «Чому я не потрапив у гілку if?». Потім робите крок до місця, де значення має змінитися. Після кроку фіксуєте спостереження: «cnt став 1 і більше не змінюється» або «умова виявилася false, бо done дорівнює false».

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

5. Налагоджувальні символи та дивна поведінка дебагера

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

По-перше, ви можете запускати не те збирання. Наприклад, ви вже виправили код, але налагоджуєте старий виконуваний файл. Це трапляється частіше, ніж хотілося б: IDE могла не перезібрати проєкт, ви могли запустити «старий» target або перемкнутися на іншу конфігурацію.

По-друге, у Release (або просто з агресивними оптимізаціями) компілятор може:

  • прибрати змінну, якщо вона вже не потрібна як окрема «сутність» (усе перетворилося на обчислення в регістрі);
  • переставити обчислення;
  • обʼєднати кілька рядків в один.

І тоді дебагер чесно скаже: «Я не можу гарантувати вам красиву послідовність кроків».

Тому правило дня просте: вчитися налагоджувати краще в Debug-збиранні, де налагоджувальні символи ввімкнені, а оптимізації або вимкнені, або мінімальні.

6. Міні‑приклади для тренування

Зараз ми зробимо те, що краще за будь-який текст: дамо мозку «відчути», навіщо потрібні зупинка та покрокове виконання. Приклади короткі: їхня мета — не написати мегапроєкт, а навчитися бачити, як рухається програма.

Приклад 1: «де я зараз?» — базові змінні

У цьому прикладі ви можете запустити програму під дебагером і покроково пройти присвоєння. На кожному кроці дивіться, як змінюються x, y, z.

#include <iostream>

int main() {
    int x = 10;
    int y = 20;
    int z = x + y;
    std::cout << z << '\n'; // 30
}

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

Приклад 2: «програма зависла» і чекає на введення

Це той випадок, який регулярно відбирає в новачків години життя. Дебагер рятує тим, що відразу показує: програма стоїть на рядку введення і чекає на дані.

#include <iostream>

int main() {
    std::cout << "Введіть n: ";
    int n = 0;
    std::cin >> n;                 // тут програма може зависнути
    std::cout << "n=" << n << '\n'; // наприклад: n=5
}

Якщо ви запустили програму під дебагером і дійшли до рядка std::cin >> n;, то «наступний крок» не відбудеться, доки ви не введете число. Це не баг. Програма просто чекає на дані.

7. Практичний приклад: TodoCLI та помилка в логіці

Тепер давайте привʼяжемо налагодження до чогось живішого, схожого на реальний навчальний проєкт. Уявімо, що в нас є маленький консольний застосунок TodoCLI: він зберігає завдання у std::vector, а в кожного завдання є прапорець done.

Сьогодні не будемо проєктувати архітектуру та інтерфейси. Візьмемо лише один фрагмент — підрахунок виконаних завдань. І спеціально припустимося помилки, яка не ламає компіляцію, зате ламає результат — ідеальний кандидат для дебагера.

Версія без помилки: «як має бути»

Спочатку — правильний варіант, щоб було з чим порівняти. Зверніть увагу: код короткий і передбачуваний, а отже — зручний для покрокового виконання.

#include <cstddef>
#include <string>
#include <vector>

struct Task {
    std::string title;
    bool done;
};

int count_done(const std::vector<Task>& tasks) {
    int cnt = 0;
    for (std::size_t i = 0; i < tasks.size(); ++i) {
        if (tasks[i].done) cnt += 1;
    }
    return cnt;
}

Якщо вам колись доведеться налагоджувати цю функцію, ви очікуватимете простої динаміки: i зростає, cnt збільшується лише тоді, коли done == true.

Версія з помилкою: «чому результат завжди дивний»

А тепер — маленька помилка, яка виглядає майже так само, компілюється, але змінює сенс. Саме в таких ситуаціях дебагер — ваш найкращий друг.

#include <cstddef>
#include <string>
#include <vector>

struct Task {
    std::string title;
    bool done;
};

int count_done(const std::vector<Task>& tasks) {
    int cnt = 0;
    for (std::size_t i = 0; i < tasks.size(); ++i) {
        if (tasks[i].done) cnt =+ 1; // ПОМИЛКА: має бути cnt += 1;
    }
    return cnt;
}

Що робить cnt =+ 1;? Це не «додати». Це «присвоїти значення +1». Тобто щоразу, коли завдання виконане, ви робите cnt = 1;.

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

Міні-main, щоб було що налагоджувати

Щоб цей фрагмент був «живим», ось маленький main(), який створює завдання й виводить результат. Він спеціально короткий, бо ми тренуємо налагодження, а не пишемо повноцінне меню.

#include <iostream>
#include <vector>

int count_done(const std::vector<struct Task>& tasks); // припустімо, оголошення вище

int main() {
    std::vector<Task> tasks{
        {"Read docs", true},
        {"Fix bug", true},
        {"Go outside", false}
    };

    std::cout << count_done(tasks) << '\n'; // очікуємо 2
}

Якщо у вашій версії count_done є помилка =+, то виведення буде 1, і це чудовий привід відкрити дебагер та подивитися, де саме логіка перестала відповідати очікуванням.

8. Типові помилки

Помилка № 1: налагоджувати програму, яку не перезібрано (або перезібрано не в тій конфігурації).
Дуже неприємний сценарій: ви виправили рядок, упевнені, що баг зник, запускаєте дебаг — а там усе те саме. Часто причина банальна: налагоджується старий виконуваний файл. Привчіть себе перед налагодженням перевіряти, чи справді ви запускаєте Debug-збирання і чи проєкт перезібрано.

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

Помилка № 3: переплутати «програму зависла» з «програма чекає на введення».
Коли виконання стоїть на std::cin >> ... або std::getline(...), програма не «зависла», вона чесно чекає на дані. Дебагер якраз і потрібен, щоб це побачити: поточний рядок — це введення. Якщо ви нічого не вводите, наступного кроку не буде.

Помилка № 4: крокувати кодом без мети й потонути в деталях.
Якщо ви просто натискаєте Step 200 разів, то швидко втомлюєтеся і починаєте ненавидіти дебагер (а він узагалі-то корисний). Спочатку сформулюйте, яке значення «підозріле», де саме воно має змінитися, і рухайтеся туди. Навіть на рівні новачка це економить купу часу.

Помилка № 5: очікувати ідеальної послідовності в оптимізованому збиранні.
У Release компілятор може «склеїти» змінні й переставити обчислення. У результаті ви бачите стрибки між рядками і «зниклі» локальні змінні. Для навчання і більшості розслідувань використовуйте Debug: так дебагеру простіше повʼязати виконання з вихідним кодом і показати значення.

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