The C Path — learn C, visually

⚙️ Compiler & Toolchain Mastery

GCC flags worth knowing by heart

⏱ 14 min · free interactive lesson · quizzes, visualizations & a real compiler

▶ Open the interactive lesson — free, no signup
Why you're learning this

The example file in this lesson contains two genuine bugs, and plain gcc compiles it without a single complaint. Add four flags and the compiler points at both broken lines before you ever run the program. A handful of well-chosen flags is the closest thing you'll get to a free expert reviewing every line you write — this lesson makes them muscle memory.

GCC accepts hundreds of options. You need maybe twenty — but those twenty are the difference between a professional build and a bug factory. Let's tour them by job: warnings, standards, optimization, debugging, and the everyday plumbing (naming output files, finding headers and libraries).

Warnings: free code review, on by request

By default GCC is scandalously quiet. Fix that — always:

Two underrated gems: -Wshadow (an inner variable hides an outer one — a silent logic-bug generator) and -Wconversion (implicit conversions that can lose data). Watch them catch two real bugs:

warn.c — compiles silently with plain gcc!
#include <stdio.h>

int total(const int *a, int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        int sum = a[i];        /* oops: shadows outer sum */
        sum += sum;
    }
    return sum;                /* always returns 0        */
}

int main(void) {
    double celsius = 36.6;
    int rounded = celsius;     /* silently truncates to 36 */
    int arr[] = {1, 2, 3};
    printf("%d %d\n", total(arr, 3), rounded);
    return 0;
}
terminal
$ gcc warn.c -o warn          # not a peep!
$ gcc -Wall -Wextra -Wshadow -Wconversion warn.c -o warn
warn.c: In function 'total':
warn.c:6:13: warning: declaration of 'sum' shadows a previous declaration [-Wshadow]
    6 |         int sum = a[i];
      |             ^~~
warn.c:4:9: note: shadowed declaration is here
    4 |     int sum = 0;
      |         ^~~
warn.c: In function 'main':
warn.c:14:19: warning: conversion from 'double' to 'int' may change value [-Wfloat-conversion]
   14 |     int rounded = celsius;
      |                   ^~~~~~~
# two genuine bugs, found for free.

🧠 Checkpoint: What does -Werror do?

  • Enables every warning GCC knows
  • Turns all warnings into hard compile errors
  • Prints warnings in red
  • Warns about error-handling bugs
Show answer

Turns all warnings into hard compile errors — It promotes warnings to errors so nobody can ignore them. Combine it with -Wall -Wextra in your own projects — but think twice before forcing it on downstream users, since future GCC versions add new warnings.

Which C? -std=

GCC defaults to a GNU dialect (currently gnu17 on most installs). Pin it explicitly: -std=c17 for portable ISO C, -std=c23 for the shiny new stuff from Part 5, or -std=gnu17 to opt into GNU extensions (next lesson!). The museum piece -ansi means -std=c90 — you'll meet it in old build scripts.

Optimization levels: the speed dial

flagmeaningtrade-off
-O0default — no optimizationfast compiles, faithful line-by-line debugging, slow code
-Ogoptimize, but keep debuggablethe sweet spot for development builds
-O1basic optimizationsrarely used directly
-O2full optimization, no size explosionthe production standard
-O3-O2 + aggressive inlining/vectorizationsometimes faster, sometimes bigger & slower — measure!
-Osoptimize for sizeembedded targets, cache-bound code
bench.c — 200 million divisions
#include <stdio.h>

int main(void) {
    double sum = 0.0;
    for (long i = 1; i <= 200000000L; i++)
        sum += 1.0 / (double)i;
    printf("%f\n", sum);
    return 0;
}
the speed dial, measured
$ gcc -O0 bench.c -o bench0 && time ./bench0
19.691044
real    0m1.98s
$ gcc -O2 bench.c -o bench2 && time ./bench2
19.691044
real    0m0.61s
$ gcc -O2 -march=native bench.c -o benchN && time ./benchN
19.691044
real    0m0.42s
# same answer every time — only the speed changes.

Same source, 3× faster — the optimizer kept sum in a register, unrolled the loop, and used vector divides. Add -march=native and GCC may also use every instruction your CPU supports (AVX2, FMA…) — great for code that runs where it's built, wrong for binaries you ship to older machines.

🧠 Checkpoint: You are stepping through code in gdb and variables keep showing <optimized out>. Best flag combo for your dev builds?

  • -O3 -g
  • -O0 alone
  • -Og -g
  • -Os -Werror
Show answer

-Og -g — -Og optimizes only in ways that don’t wreck the debugging experience, and -g provides the source mapping. -O3 aggressively deletes/merges variables; -O0 alone works too but lacks debug info without -g.

🤔 With -O2, what does GCC compile return x * 8; into — an imul?

Think first

No — a single left shift: lea eax, [0+rdi*8] (or sal eax, 3). Multiplying by a power of two is just shifting bits left, and the compiler knows it. This is why micro-optimizing x << 3 by hand in source buys you nothing but unreadable code: write x * 8 and let -O2 do its job.

Debug info and sanitizers

-g embeds DWARF debug info — the tables mapping machine code back to your source lines and variable names. Without it, gdb shows you raw addresses. It does not slow the program down; it just makes the file bigger. There is never a reason to omit -g during development.

-fsanitize=address,undefined compiles in runtime detectives that catch buffer overflows, use-after-free, signed overflow and friends the moment they happen. They're the star of the debugging-tools lesson — for now, know that the flag exists and belongs in your dev builds.

The plumbing flags

flagdoesexample
-o filename the outputgcc app.c -o app (default is the immortal a.out)
-DNAME[=val]define a macro from the command line-DDEBUG -DVERSION=3
-Idiradd a directory to the #include search path-Iinclude/
-Ldiradd a directory to the library search path-Lbuild/lib
-lnamelink against libname-lm → libm, the math library
-D, -I, -l in the wild
$ cat version.c
#include <stdio.h>
#include <math.h>
int main(void) {
#ifdef DEBUG
    fprintf(stderr, "debug build v%d\n", VERSION);
#endif
    printf("sqrt(2) = %f\n", sqrt(2));
    return 0;
}
$ gcc -DDEBUG -DVERSION=3 version.c -o version -lm
$ ./version
debug build v3
sqrt(2) = 1.414214
# no -DDEBUG?  The #ifdef block simply vanishes at preprocess time.

🧠 Checkpoint: What does -Iinclude/ do?

  • Links the library "include"
  • Adds include/ to the directories searched for #include headers
  • Installs headers into include/
  • Ignores all headers in include/
Show answer

Adds include/ to the directories searched for #include headers — Capital -I extends the header search path (used by the preprocessor). Its sibling -L extends the library search path, and lowercase -l names an actual library to link. Three different letters, three different stages.

💡

Your everyday invocation — make it muscle memory (or a shell alias):
gcc -std=c17 -Wall -Wextra -Wshadow -g -Og program.c -o program
Ship with -O2 and keep -g anyway — you can strip symbols later, but you can't conjure them back when a core dump lands on your desk.

🎉

Clang compatibility: nearly every flag on this page works identically in Clang — clang -std=c17 -Wall -Wextra -O2 just works. The two compilers deliberately share a command-line dialect, so learning one is learning both. Clang's error messages are famously friendly; try both on the same buggy file some day.

Practice

This spot has an interactive editor widget — open the interactive lesson to play with it.

Flags configure the standard compiler — but GCC also speaks a whole private dialect of C, and that's where we go next.

▶ Practice this lesson interactively (with live gcc)