CCompiler Warnings & Flags

Compiler Warnings & Flags

C is a famously permissive language. It lets you compare a signed number to an unsigned one, pass the wrong number of arguments to printf(), fall through a switch case, or use a variable before it has been given a value — and by default, the compiler will happily produce an executable anyway. The C standard defines huge swaths of this behavior as undefined or implementation-defined, which means the compiler is not required to stop you, or even tell you.

Compiler warnings are the single best tool you have for closing that gap. Modern compilers (GCC, Clang) can perform deep static analysis of your code and flag exactly the kinds of mistakes that would otherwise turn into a crash, a security hole, or a bug that only shows up on a different machine. Warnings matter enormously more in C than in languages with stricter compilers, because C simply has fewer built-in guardrails.

Essential Compiler Flags

Flag

What it does

-Wall

Enables a broad set of commonly useful warnings (despite the name, it is not actually all warnings)

-Wextra

Enables additional warnings not covered by -Wall, such as unused parameters and some signed/unsigned comparisons

-Wpedantic

Warns about anything that is not strict ISO C, including compiler-specific extensions

-Werror

Treats every warning as a hard error, so the build fails instead of silently passing

-fsanitize=address

AddressSanitizer — instruments the binary at compile time to catch buffer overflows, use-after-free, and other memory errors at runtime

-fsanitize=undefined

UndefinedBehaviorSanitizer — catches undefined behavior at runtime, such as signed integer overflow or invalid shifts

-g

Includes debug symbols, so tools like gdb and sanitizer output can point to exact source lines

Turn on warnings from day one
Always compile with at least -Wall -Wextra for every project, from the very first line of code. Retrofitting warnings onto a large, warning-free-by-neglect codebase is painful — you get flooded with hundreds of pre-existing issues at once. Starting clean means every new warning is about the line you just wrote.
A Bug Only a Warning Flag Catches

Consider a function that checks whether an index is within bounds before using it:

C
#include <stdio.h>

int get_element(int *arr, int len, int index) {
    // BUG: index is declared as int here, but compared against
    // an unsigned value below once "len" participates in the math.
    if (index >= 0 && index < len) {
        return arr[index];
    }
    return -1;
}

int main(void) {
    int data[5] = {10, 20, 30, 40, 50};
    unsigned int len = 5;   // note: unsigned
    int index = -1;

    // Mixing signed "index" with unsigned "len" in a comparison
    // silently converts index to a huge unsigned number.
    if (index < len) {
        printf("value: %d\n", data[index]); // out-of-bounds read!
    }

    return 0;
}
Compiled with no flags, this compiles cleanly and may even appear to "work" on some inputs. The comparison index < len mixes a signed int with an unsigned int. The C standard's usual arithmetic conversions silently convert index to unsigned before comparing, so -1 becomes a huge positive number — the comparison passes, and data[index] reads far out of bounds.

Compile the same file with warnings enabled:

Bash
gcc -Wall -Wextra -o prog prog.c
prog.c:19:16: warning: comparison of integer expressions of different
signedness: 'int' and 'unsigned int' [-Wsign-compare]
   19 |     if (index < len) {
      |         ~~~~~ ^ ~~~
-Wextra catches exactly this class of bug — signed/unsigned comparison — and points straight at the line. Without it, this is the kind of mistake that can sit in production code for years before triggering a crash or a security incident on the right (wrong) input.
Format string mismatches are just as dangerous
A very similar class of silent bug happens when a printf() format specifier does not match the type of its argument — for example using %d for a long or %s for a non-string pointer. GCC and Clang can check these at compile time, but only when warnings are enabled (this is covered by -Wformat, which is included in -Wall).
Recommended Baseline

Bash
# Everyday development build
gcc -std=c17 -Wall -Wextra -Wpedantic -g -fsanitize=address,undefined -o prog prog.c

# Strict CI build that fails on any warning
gcc -std=c17 -Wall -Wextra -Wpedantic -Werror -o prog prog.c
  • C allows many dangerous operations without complaint by default -- warnings are your primary safety net

  • At minimum, always build with -Wall -Wextra from the start of a project

  • -Werror in CI turns warnings into hard failures so they cannot be ignored

  • Sanitizers (-fsanitize=address, -fsanitize=undefined) catch entire classes of bugs that static warnings cannot, at the cost of runtime overhead

  • Enable warnings early -- retrofitting them onto a large codebase later is far more painful