C Basics · beginner · ~10 min
By the end of this lesson you will be able to: - Distinguish **scope** (where a name is visible) from **lifetime** (when storage exists) and explain why they are different ideas. - Identify **block scope**, **function scope**, and **file scope** in real C code, and predict exactly when each variable is created and destroyed. - Use the `static` keyword in its two distinct meanings: a **persistent local** (keeps its value between calls) and a **file-private global** (hidden from other translation units). - Explain **automatic** vs **static** storage duration and what value an uninitialized variable holds in each case. - Recognize and avoid the classic bug of **returning a pointer to a local variable**, and reason about why globals are shared across threads.
When you write a program, you give things names: variables, parameters, helper values. Two questions immediately follow every name you create. Where in the source code is this name usable? and How long does the actual memory behind it stay alive while the program runs? The first question is scope; the second is lifetime (also called storage duration). Beginners often blur them together because for a simple local variable they happen to line up — but they are genuinely separate ideas, and the moment you use static, return values, or pointers, the difference starts to matter a lot.
This lesson builds directly on Functions. There you learned that a function has its own body delimited by { }, that parameters carry values in, and that return sends a value out. Scope is what makes that isolation work: a variable named i inside one function has nothing to do with an i inside another. That isolation is not an accident or a convenience — it is a deliberate language rule that lets you write large programs without every name colliding with every other name.
In plain terms: scope is a compile-time concept — the compiler decides, by reading your braces, which names are visible at each point. Lifetime is a run-time concept — it is about real memory being reserved and released as your program executes. Keep that split in your head and the rest of this lesson falls into place: block-scoped locals live on the stack and vanish when their block ends, while file-level and static variables live for the entire run of the program.
Scope and lifetime are not academic trivia — they are where a large share of real C bugs come from.
static) to every other file in the program. That makes it easy to change state from far away by accident, which produces bugs that are extremely hard to trace.static at file scope is C's main tool for hiding implementation details. Libraries use it constantly to keep helper functions and internal state private, so callers cannot depend on — or corrupt — them.static locals are shared across threads. Knowing this is the difference between code that works and code that races and produces wrong answers under load.Getting scope and lifetime right is foundational: nearly every later topic (pointers, dynamic memory, headers, modules) assumes you understand which names are visible and how long memory lasts.
Definition. Scope is the region of source code in which a name can be used to refer to an entity. Lifetime (storage duration) is the span of run time during which the entity's storage is guaranteed to exist.
Plain explanation. Scope answers "can I type this name here and have it mean something?" Lifetime answers "is the memory still there right now?" They usually agree for ordinary locals, but static deliberately breaks the agreement: a static local has narrow scope (only one block) but long lifetime (the whole program).
SCOPE (compile time) LIFETIME (run time)
"where is the name visible?" "when does the storage exist?"
----------------------------- ------------------------------
decided by { } braces decided by storage duration
block / function / file automatic / static / (dynamic)
Knowledge check: A static int hits = 0; is declared inside one function. Is its scope wide or narrow? Is its lifetime short or long? (Answer: scope is narrow — only that function's body — but lifetime is long — the entire program run.)
Definition. A variable declared inside a { } block has block scope: it is visible from its declaration to the closing brace of that block. By default it has automatic storage duration: it is created when control reaches the declaration and destroyed when control leaves the block.
How it works internally. Automatic variables live on the call stack. Each function call pushes a stack frame holding that call's locals; returning pops the frame, instantly invalidating those locals.
Stack grows downward as calls nest:
+------------------+
| main's frame | int n; (alive whole program if main runs)
+------------------+
| next_id's frame | int local_id; (alive only during the call)
+------------------+ <- top of stack while next_id runs
(frame popped on return -> local_id storage reused)
When to use / not use. Use block-scoped locals for almost everything — they are automatically cleaned up and impossible to leak. Do not rely on them surviving after the block ends, and never hand out their address to outlive the block.
Common pitfall. An uninitialized automatic variable holds an indeterminate value (garbage), not zero. Reading it before assigning is undefined behavior.
Knowledge check (predict the output): A loop does for (int i = 0; i < 3; i++) { int t = i * i; }. Outside the loop, after it ends, can you print t or i? (Answer: No — both i and t have block scope tied to the loop; their names are not visible after the loop, and the storage is gone.)
Definition. A variable or function declared at file level (outside every function) has file scope: it is visible from its declaration to the end of the file. By default it has static storage duration (lives the whole program) and external linkage (its name is exported so other source files can refer to it).
How it works internally. File-scope variables are not on the stack. They live in a fixed region of the program's memory (the data segment, or BSS if zero-initialized) for the entire run. They are zero-initialized by default if you do not give a value.
When to use / not use. Use file-scope variables sparingly for genuinely program-wide state (e.g., a configuration loaded once). Avoid them as a lazy substitute for passing parameters — they create hidden coupling between functions.
Common pitfall. Forgetting that without static, a global's name is exported. Two files that each define int count; at file scope can cause a multiple-definition link error, or silently share one variable, depending on settings.
staticstatic is one keyword with two distinct jobs depending on where you write it:
Where you write static |
Effect on scope | Effect on lifetime | Effect on linkage |
|---|---|---|---|
| On a local (inside a function) | unchanged — still block scope | changed to whole program | n/a (locals have no linkage) |
| On a file-level variable/function | unchanged — still file scope | unchanged — already whole program | changed to internal (name hidden from other files) |
Static local — persistent state. A static local keeps its value between calls. It is initialized once, the first time control reaches it, and then survives every return. This is how a function can "remember" across calls without a global.
Call 1: counter() -> count was 0, becomes 1, returns 1
Call 2: counter() -> count is STILL 1 (survived return), becomes 2, returns 2
Call 3: counter() -> count is STILL 2, becomes 3, returns 3
File-private global. static int g_count; at file scope means "this name is private to this file." Other files cannot link to it. This is the standard way to keep module internals hidden, which you will use heavily in Header files.
Common pitfall. Assuming a static local is re-initialized on each call. It is not — static int n = 5; sets n to 5 only on the first arrival, never again.
Knowledge check (explain in your own words): Why is a static local often a cleaner choice than a global for "remember a value between calls"? (Answer: the static local has the same persistence but its name stays private to the function, so no other code can read or corrupt it — narrow scope, long lifetime.)
// FILE SCOPE -----------------------------------------------------------
int exported_global = 0; // file scope, program lifetime, EXPORTED name
static int file_private = 0; // file scope, program lifetime, name hidden
static int helper(int x) { // 'static' on a function = internal linkage
return x + 1; // (callable only from within this file)
}
void demo(int param) { // 'param' has block scope of the function body
int local = param; // BLOCK SCOPE, AUTOMATIC: dies at function end
static int calls = 0; // BLOCK SCOPE, STATIC lifetime: survives calls
calls++; // 'calls' remembers its value across calls
{ // nested block introduces a new, smaller scope
int inner = local * 2; // visible only inside these braces
(void)inner; // (silence 'unused' warning for the example)
} // 'inner' name no longer visible here
}
Key rules to remember:
} of the enclosing block (or end of file for file scope).static never changes scope — it changes lifetime (on locals) or linkage (on file-level names).Two related ideas describe how a variable behaves:
They are not the same thing. Scope is about visibility in the source code; lifetime is about memory at run time.
{} is visible only within that block. Its storage is released when the block ends.static to keep it private to that file. Without static, the name is exported and other files can refer to it.#include <stdio.h>
/* File-private state: 'static' hides this name from other source files,
and gives it program-long lifetime. */
static int g_next_id = 0;
/* Returns a fresh id each call. Uses the file-private global. */
int next_id(void) {
int local_id = ++g_next_id; /* local: created here, dies at return */
return local_id; /* the VALUE is copied out; storage is freed */
}
/* Demonstrates a static local: 'count' persists between calls,
initialized to 0 exactly once. */
int call_count(void) {
static int count = 0; /* set to 0 only on the first call */
count++; /* survives every return */
return count;
}
/* Demonstrates block scope and shadowing inside one function. */
void scope_demo(void) {
int x = 10; /* visible for the whole function body */
printf("outer x = %d\n", x);
for (int i = 0; i < 2; i++) {/* 'i' is visible only inside the loop */
int x = i + 100; /* a NEW x that shadows the outer x */
printf(" loop i=%d, inner x=%d\n", i, x);
} /* inner x and i both gone here */
printf("outer x still = %d\n", x); /* unchanged: shadowing is local */
}
int main(void) {
printf("id: %d\n", next_id()); /* 1 */
printf("id: %d\n", next_id()); /* 2 */
printf("calls: %d\n", call_count());/* 1 */
printf("calls: %d\n", call_count());/* 2 */
printf("calls: %d\n", call_count());/* 3 */
scope_demo();
return 0;
}
What it does. Three small functions each highlight one idea. next_id shows a file-private global feeding a short-lived local. call_count shows a static local that remembers across calls. scope_demo shows block scope and shadowing: the loop has its own x that does not touch the outer x.
Expected output:
id: 1
id: 2
calls: 1
calls: 2
calls: 3
outer x = 10
loop i=0, inner x=100
loop i=1, inner x=101
outer x still = 10
Edge cases to notice. count is initialized once, not per call. The inner x does not modify the outer x — shadowing only changes which name the compiler resolves, not the outer storage. After the loop, neither i nor the inner x exists.
We trace the program top to bottom.
static int g_next_id = 0; — at file scope. Storage is reserved for the whole run and zero-initialized before main starts. The static keyword hides the name from other files.next_id() call: ++g_next_id raises the global from 0 to 1, copies it into local_id, and returns 1. local_id is destroyed as the function returns — but the value 1 was already copied out.next_id() call: the global is still 1 (it persisted), ++ makes it 2, returns 2.call_count() first call: control reaches static int count = 0; for the first time, so count becomes 0. Then count++ makes it 1; returns 1.call_count() second call: the initializer is skipped (it only runs the first time). count is still 1 from before, ++ makes it 2.call_count() third call: count is 2, ++ makes it 3.scope_demo(): outer x = 10 is printed. Entering the loop, a brand-new x (the inner one) is created each iteration, shadowing the outer x. After the loop ends, the inner x and i are gone, so the final print shows the untouched outer x = 10.Trace of the persistent values:
| Call | g_next_id before |
g_next_id after |
returns |
|---|---|---|---|
| next_id #1 | 0 | 1 | 1 |
| next_id #2 | 1 | 2 | 2 |
| Call | count before |
count after |
returns |
|---|---|---|---|
| call_count #1 | 0 (init) | 1 | 1 |
| call_count #2 | 1 | 2 | 2 |
| call_count #3 | 2 | 3 | 3 |
Wrong:
int *make_value(void) {
int v = 42;
return &v; /* BUG: v dies when the function returns */
}
Why it is wrong. v has automatic storage; its stack slot is reclaimed the instant make_value returns. The returned pointer is dangling — it points at memory that no longer belongs to anyone. Dereferencing it is undefined behavior.
Corrected (return by value):
int make_value(void) {
int v = 42;
return v; /* copies the value out safely */
}
If you genuinely need a pointer that outlives the function, allocate on the heap (malloc, covered later) and document who must free it, or use a static local if a single shared instance is acceptable.
How to recognize/prevent. Compilers warn: function returns address of local variable. Treat that warning as an error.
static local to reset each callWrong belief:
int bump(void) {
static int n = 0;
n++;
return n; /* programmer expects this to always return 1 */
}
Why it is wrong. n is initialized to 0 only on the first call. It returns 1, 2, 3, ... — not 1 every time. If you wanted a fresh start each call, drop static and use a plain local.
Wrong:
int g_input; /* global used to pass data around */
void process(void) { /* reads g_input */ }
Why it is wrong. Any function can change g_input from anywhere, so behavior depends on hidden global state. Corrected: pass the data explicitly: void process(int input). Narrow scope makes code easier to reason about and test.
Compiler errors
'name' undeclared (first use in this function) — you used a name outside its scope (e.g., a loop variable after the loop, or a local from another function). Move the declaration up, or pass the value in.redefinition of 'name' / multiple definition of 'name' at link time — a file-scope variable is defined in more than one file. Make it static (file-private) or declare it extern in a header and define it once.Compiler warnings to never ignore
function returns address of local variable — a dangling-pointer bug; fix it.'x' is used uninitialized — an automatic variable read before assignment; it holds garbage.declaration of 'x' shadows a previous local (with -Wshadow) — often a sign you reused a name by accident.Runtime / logic symptoms
static; one that persists when you expected a reset means you used static by mistake.Concrete steps. Compile with -Wall -Wextra -Wshadow. For dangling-pointer suspicions, run under a sanitizer: gcc -fsanitize=address file.c then run — it reports use-after-return precisely.
Questions to ask when it does not work. Where exactly is this variable declared? When does its block end? Is its storage automatic or static? Did I take the address of something that outlives the function?
Scope and lifetime are at the heart of C memory safety.
static storage instead.static variables are zero-initialized by default.)-Wshadow to catch accidental shadowing.static variables are shared by all threads. Concurrent reads and writes without synchronization are data races (undefined behavior). Prefer passing per-thread data as parameters, or protect shared state explicitly when you reach concurrency topics.Robustness habit. Make scope as narrow as the logic allows: declare variables at first use, inside the smallest block that needs them. Narrow scope shrinks the window for misuse and makes the compiler's lifetime guarantees work for you.
Concrete uses.
static or file-scope counter, exactly like next_id above.static at file scope to hide internal helpers and state from callers, exposing only a clean API through a header. You will build this pattern in Header files.static local flag (static int initialized = 0;) lets a function set itself up on first use and skip the work afterward.static variables that persist for the device's lifetime, while keeping that state private to the driver module.Professional best-practice habits.
Beginner rules:
Advanced habits:
static at file scope to enforce module boundaries; expose state only through functions.static locals) as a concurrency liability and isolate it.Beginner 1 — Spot the scope.
Given a function with an outer variable, a loop with its own variable, and a nested block, write out (in comments) which lines each name is visible on and where each one dies. Then add a printf that would fail to compile if placed after the loop, and explain the error.
Concepts: block scope, lifetime. Hint: trace each closing brace.
Beginner 2 — Persistent greeting count.
Write void greet(const char *name) that prints Hello, <name>! and, using a static local, also prints how many times greet has been called so far (This is greeting #N). Call it three times from main.
Expected: the count goes 1, 2, 3. Hint: static int n = 0; then n++.
Intermediate 1 — Global vs. static local.
Implement the same running-total behavior twice: once with a file-scope global and once with a static local inside the function. Add a second function in the same file that tries to read the value. Note which version lets the second function touch the state and which does not, and write one sentence on why the static local is safer.
Concepts: file scope, static local, encapsulation. Hint: the global is visible to both functions; the static local is not.
Intermediate 2 — Find and fix the dangling pointer.
You are given a function char *label(int n) that fills a local char buf[16] with text and returns buf. The caller prints garbage. Diagnose why, then produce two safe fixes: (a) have the caller pass in a buffer, and (b) use a static buffer, noting the trade-off of (b).
Concepts: automatic lifetime, dangling pointer. Hint: where does buf live after return?
Challenge — Mini module with private state.
Design a tiny "ticket dispenser" in one source file: a public int take_ticket(void) that returns the next ticket number starting at 1, and a public int tickets_issued(void) that reports how many have been taken. Keep the counter as a file-private static variable so no other file can reach it. Add a separate main that takes several tickets and prints the running count. Explain, in comments, why static at file scope is the right choice here.
Concepts: file scope, static linkage, encapsulation, lifetime. Constraint: no globals with external linkage.
{ } and die when the block ends; they are not zero-initialized.static their names are exported to other files.static has two jobs: on a local it gives persistent lifetime (remembers across calls, initialized once) while keeping narrow scope; on a file-level name it gives internal linkage (private to the file).static local to reset each call, reading an uninitialized automatic, and overusing globals.static locals as shared (and thus a concurrency hazard).