1. Навіщо потрібен дебагер
Коли програма поводиться дивно, у новачка зазвичай є два інструменти: паніка та std::cout << "Я тут!\n";. Паніка не допомагає, а cout — річ корисна, але інколи він перетворює код на новорічну гірлянду з повідомлень. Дебагер дає змогу зупинити програму під час виконання й спокійно подивитися: «Що в мене у змінних? Яку гілку if насправді обрано? Чому цикл не завершується?».
Дебагер потрібен, тому що більшість багів — це не «компілятор зламався», а щось із трьох:
- програма виконується не так, як ви думаєте;
- дані всередині програми не такі, як ви думаєте;
- програма взагалі не виконується (чекає на введення, потрапила в нескінченний цикл, аварійно завершилася тощо).
Виведення через 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: так дебагеру простіше повʼязати виконання з вихідним кодом і показати значення.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ