1. Why you need a debugger
When a program behaves strangely, a beginner usually has two tools: panic and std::cout << "I'm here!\n";. Panic is useless, and cout is useful, but sometimes it turns the code into a Christmas garland of prints. A debugger is a tool that lets you pause the program during execution and calmly inspect: “What's in my variables? Which branch of the if was actually taken? Why doesn't the loop finish?”.
A debugger is useful because most bugs are not “the compiler broke”, but one of three:
- the program runs not the way you think;
- the data inside the program is not what you expect;
- the program is not running at all (waiting for input, stuck in an infinite loop, crashed, etc.).
Printing (std::cout) shows you “something” only after you inserted the print, rebuilt the project, ran it and hit the moment. A debugger shows you the state right now, and you can “advance” execution step by step.
For a mental model, keep a simple thought: “std::cout is ‘I left a note to myself’, while the debugger is ‘I stopped time and climbed into the program's head’”.
2. What the debugger does in a debug session
Imagine your program is a train running on the tracks of source code. Normally you press Run — and the train “flies” from main() to the end, and you only see the final stations (console output, result, crash).
A debugger does something else: it runs the same train but gives you a control panel. You can:
- pause the train;
- resume movement;
- take one “step” forward;
- inspect the “cargo” (variable values).
It's important to understand one principle: the debugger controls the executable file (the binary), not the “text of the code”. The text is a map, while the train runs on real tracks — machine instructions. To make the map match the tracks, you need special “hints” — debugging symbols (more on that below).
Here is a simple diagram of what happens:
flowchart LR
A["Source code (.cpp)"] --> B["Compilation (Debug)"]
B --> C["Binary + debug symbols"]
C --> D["Run under debugger"]
D --> E["Pause / inspect variables / steps"]
The word session (debug session) means: running the program under the control of a debugger. It's just a run with a control layer on top.
3. What “program paused” means
A pause in the debugger often scares people: it seems like the program “broke”. In fact pause is an operational mode: execution is temporarily frozen so you can examine everything.
It's important to understand: when the debugger highlights a line of code, it usually means: “The following will be this line (or this operation on that line)”.
So you are looking at a point in time “one millisecond before”.
Where the pause comes from
We won't dive into breakpoint internals right now (that's a separate lecture), but you should know the general picture today. A program can pause because:
- you manually hit Pause;
- you start debugging from the beginning and the IDE pauses at the start of main() (some IDEs do this);
- the program reached a pre-marked spot (usually a breakpoint);
- the program finished (the debugger just reports “done”);
- the program crashed (division by zero, out-of-bounds, etc.);
- the program is “stuck” — in reality it's waiting for input (this is a very common case).
The last item is especially important for beginners: the program does not have to be “running”. It can legitimately sit at an input line waiting for you to type a number. The debugger helps here by showing the exact line where it's waiting.
4. Stepping
If you've ever paused a video and flipped frame by frame, you almost already know how to “step”. Stepping is executing the program in small portions to see: which line executed and how values changed.
Different IDEs label the buttons slightly differently, but the basic idea is the same: commands that control the execution pace.
For us today the notion of a single step is enough:
- execute the next “visible” operation;
- pause again;
- allow you to inspect values.
Reference table (not tied to a specific IDE):
| Action | What happens | How it feels |
|---|---|---|
| Continue / Resume | The program "runs" further | Like a normal run, but you can pause again |
| Pause | The program stops at the current place | You literally "froze" execution |
| Stop | The debug session ends | The program terminates (at least for this session) |
| Step (one step) | The next operation is executed | "One frame forward" in a movie, but for code |
Why one line is not always one step
You would like to believe the debugger steps strictly line-by-line, like a teacher marking attendance. But reality is trickier: the compiler can reorder and optimize things, and one line may contain several actions.
So a good habit (it pays off 100%) is to write code so that one line contains one clear action, especially if you suspect you'll debug that spot.
For example, this is “heavy”:
int z = (x + y) * (a - b);
And this is friendlier for the debugger and your future self:
int sum = x + y;
int diff = a - b;
int z = sum * diff;
Yes, there are more lines. But you can step through and verify each idea: “Is sum really that? Is diff non-negative?”.
How to think while stepping
Stepping can turn into meaningless “click-click-click on the Step button” if you don't ask yourself the right question. So keep a very practical trio in mind.
First, form the question: “Why isn't cnt increasing?” or “Why didn't I enter the if branch?”. Then step to the place where the value should change. After the step, record your observation: “cnt became 1 and doesn't change” or “the condition was false because done is false”.
It's almost like detective work, except the culprit is one line of code and its alibi is ironclad: the compiler let it pass.
5. Debug symbols and weird debugger behavior
Sometimes the debugger behaves like a cat: it pretends it doesn't know you. Variables are "optimized away", steps jump, lines don't match. Usually this is not mystical, but one of the causes below.
First, you might be running the wrong build. For example, you changed the code but are debugging an old binary. This happens more often than you'd like: the IDE might not have rebuilt the project, you might have launched an "old" target, or switched to a different configuration.
Second, in Release (or with heavy optimizations) the compiler is allowed to:
- remove a variable if it doesn't exist as an entity (it becomes a computation in a register);
- reorder computations;
- collapse several lines into one.
And the debugger will honestly show: “I can't guarantee nice line-by-line behavior.”
So the rule of the day is simple: learn to debug in a Debug build, where debug symbols are enabled and optimizations are either disabled or minimal.
6. Mini-examples for practice
Now we'll do what any text can't beat: let your brain "feel" why pause and step are useful. Examples are short: their goal is not to write a mega-project, but to train you to see program movement.
Example 1: "where am I?" — basic variables
In this example you can run the program under the debugger and step through the assignments. At each step, watch how x, y, z change.
#include <iostream>
int main() {
int x = 10;
int y = 20;
int z = x + y;
std::cout << z << '\n'; // 30
}
If you step through, you should see a simple picture: first x appears, then y, then z is computed. It's boring, but it's the "ABC" of debugging: you learn that execution is not magic but a sequence of actions.
Example 2: "the program is stuck" and waiting for input
This is the case that regularly costs beginners hours of life. The debugger helps by showing that the program is stopped on an input line and waiting for data.
#include <iostream>
int main() {
std::cout << "Enter n: ";
int n = 0;
std::cin >> n; // here the program may "hang" waiting for input
std::cout << "n=" << n << '\n'; // e.g.: n=5
}
If you run under the debugger and reach the line std::cin >> n;, the "next step" will not happen until you provide a number. This is not a bug. It's literally the input contract: “give me data”.
7. Practical example: TodoCLI and a logic bug
Now let's tie debugging to something more "real", similar to an actual learning project. Imagine a small console app TodoCLI that stores tasks in a std::vector, and a task has a done flag.
We won't design the architecture or interfaces today. We'll take a single fragment: counting completed tasks. And intentionally introduce a bug that does not break compilation but breaks the result — a perfect candidate for the debugger.
Correct version: "how it should be"
First — the correct variant so you have something to compare with. Note: the code is short and predictable, so it's convenient for stepping.
#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;
}
If you ever debug this function, you expect a simple dynamic: i increases, cnt increases only when done is == true.
Version with a bug: "why is the result always weird"
Now a small mistake that looks almost identical, compiles, but changes the meaning. The situation where the debugger is your best friend.
#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; // BUG: should be cnt += 1;
}
return cnt;
}
What does cnt =+ 1; do? It's not "add". It's "assign the value +1". So each time a task is done you set cnt = 1;.
How the debugger helps to see this step by step (pure mechanics): you start debugging, get to the loop and step through, watching two values: i and cnt. You will see cnt become 1 on the first completed task… and remain 1 for the following ones, although logically it should grow. At that moment you realize: “Ah, the problem is the line that changes cnt”.
Mini main to have something to debug
To make this piece "live", here is a small main() that creates tasks and prints the result. It's intentionally short because we train debugging, not build a full menu.
#include <iostream>
#include <vector>
int count_done(const std::vector<struct Task>& tasks); // assume declaration above
int main() {
std::vector<Task> tasks{
{"Read docs", true},
{"Fix bug", true},
{"Go outside", false}
};
std::cout << count_done(tasks) << '\n'; // expect 2
}
If your version of count_done has the =+ bug, the output will be 1, and that's a great reason to open the debugger and see where exactly the logic stopped matching expectations.
8. Common mistakes
Mistake #1: debugging a program that wasn't rebuilt (or was built in the wrong configuration).
A very unpleasant scenario: you fixed a line, are sure the bug is gone, start debugging — and it's the same. Often the cause is trivial: you're debugging an old binary. Make it a habit before debugging to ensure you are actually running the Debug build and the project is rebuilt.
Mistake #2: assuming “if the debugger paused — the program broke”.
A pause is an operational mode. The debugger can pause by your command, at the start of main(), or because the program reached a checkpoint. First look where and why it paused, and only then conclude “this is a bug”.
Mistake #3: confusing “the program is stuck” with “the program is waiting for input”.
When execution is at std::cin >> ... or std::getline(...), the program is not "stuck", it is legitimately waiting for data. The debugger is exactly what you need to see: the current line is an input. If you don't provide input, the next step won't happen.
Mistake #4: stepping through code without a goal and drowning in details.
If you just press Step 200 times, you'll get tired and start hating the debugger (which is actually useful). First formulate which value looks suspicious and where it should change, and go there. Even at a beginner level this saves a lot of time.
Mistake #5: expecting perfect line-by-line behavior in an optimized build.
In Release the compiler may "glue" variables and reorder computations. As a result you'll see line jumps and "missing" local variables. For learning and most investigations use Debug: this makes it easier for the debugger to map execution to source and show values.
GO TO FULL VERSION