Debugging and Best Practices
Every C programmer spends a lot of time finding bugs. This lesson covers the tools that make it faster — compiler warnings, a debugger, sanitizers and assertions — and the habits that prevent bugs in the first place.
Step 1: let the compiler help
Compile every build with warnings on, and fix them all. A strong set for development:
gcc -std=c17 -Wall -Wextra -pedantic -Wshadow -Wconversion -g -O0 \
-fsanitize=address,undefined main.c -o main
| Option | Why |
|---|---|
-Wall -Wextra -pedantic | The main warning sets |
-Wshadow, -Wconversion | Shadowed names and lossy conversions |
-g | Debug information: variable names and line numbers for the debugger |
-O0 | No optimisation, so the debugger follows your code line by line |
-fsanitize=address,undefined | Run-time checks for memory errors and UB (lessons 38, 54) |
For releases, switch to -O2 and drop the sanitizers.
Step 2: print debugging, done well
Quick printfs are a perfectly good first tool. Print to stderr (it's unbuffered, so the message appears even if the program crashes right after) and include where you are:
#include <stdio.h> #define TRACE(fmt, ...) fprintf(stderr, "%s:%d: " fmt "\n", __func__, __LINE__, __VA_ARGS__) int digit_sum(int n) { int sum = 0; while (n > 0) { TRACE("n=%d sum=%d", n, sum); sum += n % 10; n /= 10; } return sum; } int main(void) { printf("result: %d\n", digit_sum(472)); return 0; }
digit_sum:8: n=472 sum=0 digit_sum:8: n=47 sum=2 digit_sum:8: n=4 sum=9 result: 13
__func__ holds the current function's name.
Step 3: use a debugger
A debugger runs your program under control: stop at a line, step through it, and inspect variables. GDB is standard on Linux, LLDB on macOS; IDEs like VS Code and CLion put a graphical interface on top of them.
| Task | GDB | LLDB |
|---|---|---|
| Start | gdb ./main | lldb ./main |
| Breakpoint at a function / line | break digit_sum / break main.c:12 | b digit_sum / b main.c:12 |
| Run (with arguments) | run arg1 arg2 | run arg1 arg2 |
| Next line (step over calls) | next | next |
| Step into a call | step | step |
| Print a variable | print sum | p sum |
| Show the call stack | backtrace | bt |
| Continue / quit | continue / quit | c / q |
When a program crashes, running it in the debugger and typing backtrace shows exactly which function calls led to the crash — often the fastest way to find the cause.
Step 4: assertions
assert documents and checks assumptions; a failed assertion stops the program with the file, line and condition:
#include <stdio.h> #include <assert.h> double percentage(int part, int whole) { assert(whole > 0); return 100.0 * part / whole; } int main(void) { printf("%.1f%%\n", percentage(3, 8)); printf("%.1f%%\n", percentage(1, 0)); return 0; }
37.5% Assertion failed: (whole > 0), function percentage, file main.c, line 5.
The exact wording differs between C libraries. Assertions are for programmer errors; handle bad input with normal error checks (lesson 50).
Step 5: test
Small functions with clear inputs and outputs are easy to test. Even without a framework, a test program with assertions catches regressions:
#include <stdio.h> #include <assert.h> #include <string.h> int count_vowels(const char *s) { int n = 0; for (; *s; s++) { if (strchr("aeiouAEIOU", *s)) n++; } return n; } int main(void) { assert(count_vowels("") == 0); assert(count_vowels("rhythm") == 0); assert(count_vowels("Education") == 5); assert(count_vowels("AEIOU aeiou") == 10); printf("all tests passed\n"); return 0; }
all tests passed
Frameworks such as Unity, Check, cmocka and Criterion add test discovery and nicer reports. Run your tests with sanitizers on.
Best practices checklist
- Warnings on, sanitizers in testing, zero warnings in the build.
- Initialise everything: variables, arrays, pointers (
NULL). - Check every return value that can signal failure: allocation, file and input functions.
- Keep sizes with arrays and use bounded functions:
fgets,snprintf, nevergets. - One owner per allocation; free in one place; set freed pointers to
NULL. consteverything you don't modify;staticeverything that doesn't need to be visible from other files.- Small functions with clear names and one job.
- Prefer clarity to cleverness: one side effect per statement, parentheses where precedence is unclear, functions instead of tricky macros.
- Use a formatter (
clang-format) and a static analyser (clang-tidy,cppcheck). - Version control your code with Git — see the Git course.
Try it yourself
This function is meant to return the index of the largest element, but has a bug. Find it with print statements or a debugger, then fix it.
largest is at index 3