🧬 Types & Qualifiers, In Depth
auto, register, static, extern: who lives where, seen by whom
▶ Open the interactive lesson — free, no signupYou'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":
| linkage | who can reference it | how you get it |
|---|---|---|
| external | any translation unit | file-scope things, by default |
| internal | this translation unit only | file-scope + static |
| none | just its own scope | locals, 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 */
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 */
#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;
}$ 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 written | keyword | duration | linkage |
|---|---|---|---|
| in a function | (none / auto) | the block | none |
| in a function | static | whole program | none |
| file scope | (none) | whole program | external |
| file scope | static | whole program | internal |
| anywhere | extern | — | refers 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.