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
OptionWhy
-Wall -Wextra -pedanticThe main warning sets
-Wshadow, -WconversionShadowed names and lossy conversions
-gDebug information: variable names and line numbers for the debugger
-O0No optimisation, so the debugger follows your code line by line
-fsanitize=address,undefinedRun-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:

C
#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;
}
Output
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.

TaskGDBLLDB
Startgdb ./mainlldb ./main
Breakpoint at a function / linebreak digit_sum / break main.c:12b digit_sum / b main.c:12
Run (with arguments)run arg1 arg2run arg1 arg2
Next line (step over calls)nextnext
Step into a callstepstep
Print a variableprint sump sum
Show the call stackbacktracebt
Continue / quitcontinue / quitc / 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:

C
#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;
}
Output
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:

C
#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;
}
Output
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, never gets.
  • One owner per allocation; free in one place; set freed pointers to NULL.
  • const everything you don't modify; static everything 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.

Try it yourself · main.c
Edit the code, copy it into main.c, then compile and run it with cc -std=c17 -Wall main.c -o main && ./main. How to set up a C compiler
Output
largest is at index 3