The C Path — learn C, visually

🧬 Types & Qualifiers, In Depth

auto, register, static, extern: who lives where, seen by whom

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

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

You've already met a variable that quietly survives between function calls — that was static at work, one of four small keywords with outsized power. And the moment your project grows past a single file, the same keywords decide which names other files can use and which stay hidden: static doubles as C's version of "private".

Every variable in a C program has two hidden properties: a storage duration (how long it exists) and a linkage (which parts of the program can refer to it by name). Four keywords — auto, register, static, extern — are how you control both; C calls them the storage classes.

The two easy ones: auto and register

auto means "ordinary local variable, dies with its block" — which is the default for locals, so nobody ever writes it. (C23 recycled the keyword for type inference, auto x = 3.14;, giving it its first real job in 50 years.)

register asks the compiler to keep a variable in a CPU register. Modern optimizers ignore the hint — they allocate registers far better than you — but one enforceable effect remains: you cannot take the address of a register variable. &r is a compile error.

🧠 Checkpoint: What is the only enforced effect of register int r; in modern C?

  • r is guaranteed to live in a CPU register
  • Programs run measurably faster
  • Taking &r is a compile error
  • r cannot be modified
Show answer

Taking &r is a compile error — The register hint is freely ignorable (and ignored), but the address-of ban is real: something with no memory address cannot yield one, so the standard forbids &r outright.

static, meaning #1: the local that never dies

Inside a function, static moves a variable from the stack to static storage. It's initialized once, before main runs, and keeps its value between calls — while remaining visible only inside its function:

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

🧠 Checkpoint: How many times does static int id = 0; initialize id across 1000 calls?

  • 1000 times
  • Once per program run, before main begins
  • Once per call, but only if id changed
  • Zero — statics are never initialized
Show answer

Once per program run, before main begins — Static-duration objects are initialized exactly once, at program startup (conceptually — the value is baked into the .data segment of the executable). The = 0 line is not executable code.

Translation units and linkage: the mental model

Each .c file (after preprocessing) is a translation unit, compiled in complete isolation. The linker later stitches them together by matching names. Linkage is a name's "networking setting":

linkagewho can reference ithow you get it
externalany translation unitfile-scope things, by default
internalthis translation unit onlyfile-scope + static
nonejust its own scopelocals, parameters

static, meanings #2 and #3: privacy at file scope

At file scope, static means something completely different: internal linkage. A static global or a static function is invisible to every other .c file — it's private, C style. Marking every helper function static is a hallmark of well-organized C: it prevents name collisions across files and tells readers "the whole story of this function is right here."

extern: "it exists, but elsewhere"

extern turns a definition into a mere declaration — a promise to the compiler that some other translation unit defines the object. Here's the whole dance in two files:

counter.c — the defining file
/* counter.c */
static int secret = 0;      /* internal linkage: THIS file only */
int counter = 0;            /* external linkage: the definition */

static void audit(void) {   /* private helper — file-local      */
    /* ... */
}

void bump(void) {           /* external: callable from anywhere */
    secret++;
    counter++;
    audit();
}
main.c — the using file
/* main.c */
#include <stdio.h>

extern int counter;   /* declaration: "defined in another TU" */
void bump(void);      /* function declarations are extern-by-default */

int main(void) {
    bump(); bump(); bump();
    printf("counter = %d\n", counter);

    /* audit();            link error: internal to counter.c  */
    /* printf("%d", secret);  compile error: not declared here */
    return 0;
}
terminal
$ gcc main.c counter.c -o app && ./app
counter = 3
# each .c compiled separately; the linker matched 'counter' and 'bump'
⚠️

Definition vs declaration: int counter = 0; allocates storage — exactly one TU may do this. extern int counter; allocates nothing and may appear in a thousand files (put it in a header!). A file-scope int counter; with no initializer is a tentative definition — it becomes a real zero-initialized definition if nothing else in the TU defines it. Historically linkers merged duplicate tentative definitions across files ("common symbols"), but since GCC 10 (-fno-common default) duplicates are a link error — as the standard always intended. Define once; extern everywhere else.

🧠 Checkpoint: A helper function in parse.c should not be callable from other files. The idiomatic fix?

  • Name it with a leading underscore
  • Declare it static — internal linkage hides it from the linker
  • Declare it extern
  • Move it inside main()
Show answer

Declare it static — internal linkage hides it from the linker — static at file scope is C's "private". Bonus effects: no risk of colliding with a same-named function elsewhere, and the compiler can optimize harder (even discard it entirely after inlining) since it can see every caller.

Cheat sheet

where writtenkeyworddurationlinkage
in a function(none / auto)the blocknone
in a functionstaticwhole programnone
file scope(none)whole programexternal
file scopestaticwhole programinternal
anywhereexternrefers to external

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

One function-shaped keyword remains in the storage-class family tree, and it's the strangest of the bunch: inline.

▶ Practice this lesson interactively (with live gcc)