🌱 C Basics
Scope & lifetime: who sees what, and for how long
▶ Open the interactive lesson — free, no signupTwo mysteries ambush every beginner: "why isn't my variable changing?!" (a hidden twin with the same name is soaking up your assignments) and "how can a function remember something between calls?" (a variable that quietly refuses to die). Both answers live in this lesson — the rules of who can see a variable and how long it stays alive.
Every variable in C answers two independent questions. Scope: from where in the code can I refer to it? Lifetime: for how long does its memory exist? Most of the time the two travel together — but the interesting corners of C are exactly where they split apart. First up: what a pair of braces — a block — does to a variable's visibility, including the sneaky case where one variable hides another behind the same name.
Block scope & shadowing
A variable declared inside { … } exists from its declaration to the closing brace — that region is its scope. Blocks nest, and an inner declaration with the same name shadows the outer one: the outer variable still exists, but its name is temporarily eclipsed.
#include <stdio.h>
int main(void) {
int x = 1;
printf("outer x: %d\n", x);
{
int x = 2; /* shadows the outer x */
printf("inner x: %d\n", x);
x = 99; /* changes ONLY the inner x */
} /* inner x dies here */
printf("outer x: %d (untouched)\n", x);
return 0;
}$ gcc shadow.c -o shadow && ./shadow outer x: 1 inner x: 2 outer x: 1 (untouched)
Shadowing is legal and occasionally handy, but it's a classic source of "why isn't my variable changing?!" confusion. GCC's -Wshadow flags every case — worth turning on.
🧠 Checkpoint: Inside the inner block of shadow.c, what happened to the outer x?
- It was destroyed and recreated later
- It still existed — only its NAME was hidden by the inner x
- It was renamed by the compiler
- It became read-only
Show answer
It still existed — only its NAME was hidden by the inner x — Shadowing hides names, not memory. The outer x sat in its stack slot the whole time, value intact — there was just no way to spell it while the inner x owned the name.
Two historic keywords: auto and register
Ordinary locals are automatic — created on block entry, destroyed on exit, stack-dwelling. The keyword auto says exactly that… and since locals are automatic by default, nobody has typed it in fifty years. (Plot twist: C23 recycled auto for type inference, like C++.) Its sibling register once begged the compiler to keep a variable in a CPU register; modern optimizers ignore the hint entirely. Its one surviving effect: you may not take a register variable's address. Both keywords are museum pieces you must recognize, not use.
static locals: scope of an ant, lifetime of an elephant
Mark a local variable static and its scope stays tiny — just that block — but its lifetime becomes the whole program. It lives in the data segment (Part 0!), not the stack, is initialized exactly once before main even starts, and remembers its value between calls:
#include <stdio.h>
int next_id(void) {
static int id = 100; /* initialized ONCE, lives forever */
id++;
return id;
}
int main(void) {
printf("%d\n", next_id());
printf("%d\n", next_id());
printf("%d\n", next_id());
return 0;
}$ gcc counter.c -o counter && ./counter 101 102 103 # a plain 'int id = 100;' would print 101 three times
Watch the initialization happen only once — the trace makes it obvious:
▶ This spot has an interactive trace widget — open the interactive lesson to play with it.
🧠 Checkpoint: How many times does static int id = 100; execute its initialization across 5 calls to next_id()?
- 5 times
- Twice
- Exactly once, before the program starts
- Never — statics start as garbage
Show answer
Exactly once, before the program starts — Static storage is initialized once, at program startup (and to zero if you give no initializer — never garbage, unlike stack locals!). On later calls the declaration line is just scenery.
extern: one variable, many files
A variable declared outside every function is global: file-wide scope, program-long lifetime. To use one global across several .c files, exactly one file defines it, and the others declare it with the keyword extern — "it exists, but elsewhere; linker, please connect us":
/* ---- config.c ---- */
int max_users = 64; /* THE definition: memory lives here */
/* ---- server.c ---- */
#include <stdio.h>
extern int max_users; /* declaration: defined elsewhere */
void report(void) {
printf("limit: %d users\n", max_users); /* same variable */
}This is the declaration-vs-definition split from the functions lesson, replayed for variables. And that's linkage in a nutshell — the linker matching names across files. One more twist: static on a global (or a function) means the opposite of extern — "private to this file, invisible to the linker". Same keyword, second job; Part 3's storage-classes lesson dissects it fully.
Style compass: prefer the smallest scope that works. Locals over globals, loop-scoped counters over function-wide ones. Globals aren't evil, but every one of them is a variable ANY code might change — the more you have, the harder your program is to reason about.
🧠 Checkpoint: extern int max_users; means…
- create a new variable named max_users
- this variable exists, but its definition (memory) is in another file — linker, connect us
- max_users cannot be modified
- max_users is stored on the heap
Show answer
this variable exists, but its definition (memory) is in another file — linker, connect us — extern makes a declaration without a definition: no storage is allocated. Exactly one translation unit must define the variable, or the linker reports undefined reference — the same promise/delivery dance as function prototypes.
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
🎉 That wraps Part 1 — you can now write real, structured C programs. Ahead lies the part that gives C its reputation and its power: pointers and memory. Deep breath; it's more logical than the legends claim.
▶ Practice this lesson interactively (with live gcc)