Debugging
Your code will have bugs. Everyone’s code has bugs. The difference between beginners and experienced programmers isn’t that experts write perfect code - it’s that they know how to find and fix problems quickly.
This lesson teaches you the art of debugging - tracking down what’s wrong and making it right.
Two Kinds of Errors
Before your program even runs, you might hit two different kinds of problems:
Compiler errors happen when you try to build your program. The compiler can’t understand your code, so it refuses to create an executable. You have to fix these before you can run anything.
Runtime errors happen while your program is running. The code compiled fine, but something goes wrong when you actually execute it. Maybe it crashes, maybe it gives wrong answers, maybe it runs forever.
Learning to tell these apart is the first step in debugging.
Reading Compiler Errors
Compiler errors look scary at first. Here’s a typical one:
main.c:7:12: error: expected ';' before 'return'
7 | printf("Hello")
| ^
8 | return 0;
| ~~~~~~Let’s break this down:
main.c- the file with the problem7- the line number12- the column (how far across the line)error:- this is an error (as opposed to a warning)expected ';' before 'return'- what went wrong
The compiler is telling you: “On line 7, around column 12, I expected a semicolon but found something else.”
Look at line 7 - the printf statement is missing its semicolon. Add it and the error goes away.
Common Compiler Errors and What They Mean
“undeclared identifier” or “use of undeclared identifier”
error: 'countr' undeclaredYou’re using a variable or function that doesn’t exist. Usually a typo - you probably meant counter.
“expected ‘;’ before…”
error: expected ';' before 'int'The line above is missing a semicolon.
“implicit declaration of function”
warning: implicit declaration of function 'prntf'You’re calling a function the compiler doesn’t know about. Either you misspelled it (prntf vs printf) or you forgot to #include the right header.
“conflicting types”
error: conflicting types for 'calculate'You declared a function one way but defined it differently. Maybe the return types don’t match, or the parameters are different.
“expected ‘)’ before…”
error: expected ')' before ';' tokenMismatched parentheses. Count your opening and closing parens.
“assignment to expression with array type”
error: assignment to expression with array typeYou tried to assign to an array directly. Arrays can’t be reassigned - you need to copy elements one by one or use strcpy for strings.
The First Error Matters Most
When the compiler spits out 50 errors, don’t panic. Fix the first one and recompile. Often a single mistake (like a missing brace) confuses the compiler so much that it reports dozens of fake errors afterward.
Fix errors from the top down, recompiling after each fix.
Printf Debugging
The simplest debugging technique is also the most useful: print stuff out.
#include <stdio.h>
int calculate_total(int items[], int count) {
int total = 0;
printf("DEBUG: Starting calculation, count = %d\n", count);
for (int i = 0; i <= count; i++) { // Bug: should be i < count
printf("DEBUG: i = %d, items[i] = %d\n", i, items[i]);
total += items[i];
printf("DEBUG: Running total = %d\n", total);
}
printf("DEBUG: Final total = %d\n", total);
return total;
}By printing values at each step, you can see exactly what’s happening. When the output doesn’t match what you expect, you’ve found your bug.
Printf Debugging Tips
Print variable values, not just “got here”:
// Not very helpful
printf("checkpoint 1\n");
// Much more helpful
printf("checkpoint 1: x=%d, y=%d, result=%d\n", x, y, result);Add labels so you know which print is which:
printf("[calculate_total] Starting with count=%d\n", count);
printf("[calculate_total] Loop iteration i=%d\n", i);Print before AND after suspicious code:
printf("Before mystery_function: x = %d\n", x);
mystery_function(&x);
printf("After mystery_function: x = %d\n", x);Use a DEBUG flag to turn prints on and off:
#define DEBUG 1
#if DEBUG
printf("Debug info: x = %d\n", x);
#endifSet DEBUG to 0 when you don’t want the output cluttering things up.
Using assert()
The assert() function is a powerful debugging tool. It checks if a condition is true. If it’s not, the program stops immediately and tells you what went wrong.
#include <stdio.h>
#include <assert.h>
int divide(int a, int b) {
assert(b != 0); // Program stops here if b is zero
return a / b;
}
int main(void) {
printf("%d\n", divide(10, 2)); // Works fine
printf("%d\n", divide(10, 0)); // Crashes with assertion failure
return 0;
}When the assertion fails, you get a message like:
Assertion failed: b != 0, file main.c, line 5When to Use assert()
Use assertions to check things that should never happen:
// Array index should always be valid
assert(index >= 0 && index < array_size);
// Pointer should never be NULL after allocation
int *data = malloc(100 * sizeof(int));
assert(data != NULL);
// Function should only receive positive values
void process_positive(int value) {
assert(value > 0);
// ... rest of function
}Assertions are documentation that enforces itself. They say “this must be true” and prove it by crashing if it’s not.
assert() vs Error Handling
Don’t use assertions for things that might legitimately go wrong in normal use:
// BAD: User input could be anything
assert(user_input > 0);
// GOOD: Check and handle gracefully
if (user_input <= 0) {
printf("Please enter a positive number\n");
return -1;
}Assertions are for catching programmer mistakes during development. Error handling is for dealing with real-world problems in production.
Introduction to GDB
Printf debugging works, but for serious bugs, you want a real debugger. GDB (GNU Debugger) lets you pause your program, look at variables, and step through code line by line.
Compiling for Debugging
First, compile with the -g flag. This adds debugging information to your program:
gcc -g -o myprogram myprogram.cWithout -g, the debugger won’t know how your machine code relates to your source code.
Starting GDB
Run GDB with your program:
gdb ./myprogramYou’ll see a (gdb) prompt. Your program isn’t running yet - you’re just inside the debugger.
Essential GDB Commands
run (or just r) - Start your program:
(gdb) runbreak (or b) - Set a breakpoint. The program pauses when it reaches this line:
(gdb) break main
(gdb) break 15
(gdb) break calculate_totalYou can break at a function name or a line number.
next (or n) - Execute the next line, stepping over function calls:
(gdb) nextstep (or s) - Execute the next line, stepping into function calls:
(gdb) stepThe difference: if the line calls a function, next runs the whole function, while step goes inside it.
print (or p) - Show the value of a variable:
(gdb) print x
(gdb) print array[5]
(gdb) print *pointerbacktrace (or bt) - Show how you got here (the call stack):
(gdb) backtraceThis shows which function called which. Essential when a crash happens deep in your code.
continue (or c) - Keep running until the next breakpoint:
(gdb) continuequit (or q) - Exit GDB:
(gdb) quitA GDB Session Example
Say you have a buggy program:
#include <stdio.h>
int sum_array(int arr[], int size) {
int total = 0;
for (int i = 0; i <= size; i++) { // Bug: should be <
total += arr[i];
}
return total;
}
int main(void) {
int numbers[] = {10, 20, 30};
int result = sum_array(numbers, 3);
printf("Sum: %d\n", result);
return 0;
}Debug it like this:
$ gcc -g -o buggy buggy.c
$ gdb ./buggy
(gdb) break sum_array
Breakpoint 1 at 0x1234: file buggy.c, line 4.
(gdb) run
Breakpoint 1, sum_array (arr=0x7fff..., size=3) at buggy.c:4
4 int total = 0;
(gdb) next
5 for (int i = 0; i <= size; i++) {
(gdb) next
6 total += arr[i];
(gdb) print i
$1 = 0
(gdb) print arr[i]
$2 = 10
(gdb) continue
... (let it run, see what happens)By stepping through and checking values, you can watch the bug happen in slow motion.
Common Runtime Bugs
Some bugs only show up when you run the program. Here are the classics:
Segmentation Faults (Segfaults)
A segfault means your program tried to access memory it shouldn’t. Common causes:
Dereferencing NULL:
int *ptr = NULL;
*ptr = 5; // CRASHArray out of bounds:
int arr[10];
arr[100] = 5; // CRASH (or worse - silent corruption)Using freed memory:
int *data = malloc(sizeof(int));
free(data);
*data = 5; // CRASHStack overflow from infinite recursion:
void infinite(void) {
infinite(); // Eventually crashes
}When you get a segfault, run your program in GDB. It will stop at the crash and show you where it happened. Use backtrace to see how you got there.
Infinite Loops
Your program runs forever and never produces output:
int i = 0;
while (i < 10) {
printf("%d\n", i);
// Forgot to increment i!
}When this happens, press Ctrl+C to stop the program. Add printf statements inside the loop to see what’s happening. Check that your loop condition can actually become false.
Off-by-One Errors
One of the most common bugs in programming. You’re almost right, but you’re one off:
// Bug: processes 11 elements instead of 10
for (int i = 0; i <= 10; i++) { ... }
// Bug: misses the last element
for (int i = 0; i < size - 1; i++) { ... }
// Bug: reads one past the end of the string
char str[10];
for (int i = 0; i <= strlen(str); i++) { ... }The fix is usually changing <= to < or vice versa, or adjusting the start/end values.
Remember: arrays are zero-indexed. An array of size 10 has valid indices 0 through 9, not 1 through 10.
Uninitialized Variables
Local variables in C start with garbage values:
int count; // Could be anything!
count++; // Adding 1 to garbageAlways initialize your variables:
int count = 0;The compiler might warn you about this. Pay attention to warnings.
Debugging Strategies
Knowing the tools is one thing. Knowing how to think about bugs is another.
Binary Search for Bugs
If your program is large and something is wrong, don’t read through the whole thing. Use binary search:
- Find a point roughly in the middle of your code
- Add a print statement there
- Run the program
- Is the output correct at that point?
- Yes: The bug is in the second half
- No: The bug is in the first half
- Repeat, narrowing down each time
This finds bugs in log(n) time instead of n time. In a 1000-line program, you can find the problem in about 10 steps.
Rubber Duck Debugging
This sounds silly but it works. Explain your code, line by line, to a rubber duck (or any inanimate object). Explain what each line does and why.
The act of explaining forces you to think carefully. You’ll often say “and this line does… wait, that’s not right.”
You don’t need an actual duck. A coffee mug works. Or just explain it out loud to yourself. The key is verbalizing your assumptions.
Simplify the Problem
Can’t figure out what’s wrong? Make a smaller version of the problem:
- Copy your buggy function to a new file
- Write a simple main() that just calls that function
- Hardcode the inputs that cause the bug
- Remove everything that isn’t needed to reproduce the bug
Now you have a tiny program that shows the bug. This is much easier to debug than a 5000-line project.
Check Your Assumptions
When you’re stuck, write down what you think is true:
- “The array has 10 elements”
- “The pointer is not NULL here”
- “The loop runs exactly 5 times”
Then verify each assumption with print statements or the debugger. Often you’ll find one of your assumptions is wrong.
Read the Error Message Again
This sounds obvious, but do it anyway. Read the error message slowly, word by word. Beginners often glance at errors and assume they understand. The message usually tells you exactly what’s wrong - you just have to actually read it.
Try It Yourself
Create a program with a deliberate off-by-one error in a loop. Use printf debugging to find exactly where it goes wrong.
Write a function that crashes if given a NULL pointer. Use assert() to make the crash obvious. Then use GDB to see the backtrace when the assertion fails.
This code has a bug. Find it using any debugging technique:
int find_max(int arr[], int size) { int max = 0; for (int i = 0; i < size; i++) { if (arr[i] > max) { max = arr[i]; } } return max; }(Hint: What if all numbers are negative?)
Practice GDB: Write a simple program with a few functions that call each other. Set breakpoints, step through, and use
printto examine variables. Get comfortable with the commands.Take some old code you wrote that has a bug you never fixed. Apply the debugging strategies from this lesson to finally track it down.
Common Mistakes
Ignoring warnings. Compiler warnings often point directly at bugs. Compile with
-Wallto see all warnings, and fix them.Debugging with stale code. Make sure you recompile after making changes. Many frustrated debugging sessions happen because someone forgot to rebuild.
Not checking return values. Functions like
malloc,fopen, andscanftell you when they fail. If you ignore their return values, bugs become invisible.Assuming you know where the bug is. You think you know, but you might be wrong. Verify before you spend an hour staring at the wrong function.
Making multiple changes at once. Change one thing, test. Change another thing, test. If you change five things and it starts working, you don’t know which change fixed it (or if you introduced new bugs).
Not using version control. Git lets you go back to when things worked. If you break something badly, you can revert. Without version control, you might lose hours of work.
The Debugging Mindset
Debugging is detective work. Something went wrong. You have clues (error messages, wrong output, crashes). Your job is to follow the evidence to the culprit.
Stay calm. Stay methodical. Don’t just randomly change things and hope for the best. Form a hypothesis, test it, and repeat.
The best debuggers aren’t the ones who never have bugs. They’re the ones who systematically track bugs down and squash them, every single time.
Next Up
In Part 28, we’ll put everything together in a complete practical project that uses all the skills you’ve learned.
Enjoyed This?
If this helped something click, subscribe to my YouTube channel. More content like this, same approach - making things stick without insulting your intelligence. It’s free, it helps more people find this stuff, and it tells me what’s worth making more of.