Scope, Storage Duration, and Lifetime
This chapter separates three concepts that beginners frequently conflate: scope (where a name is visible), storage duration (how long storage is guaranteed to exist), and lifetime (the period during which an object can be accessed). Nearly all pointer and memory bugs trace back to confusing these.
Why This Matters
"Use-after-free," "dangling pointer," and "returning a pointer to a local" are all lifetime bugs, not scope bugs. Understanding the difference between "the name is out of scope" and "the object is dead" is what lets you reason correctly about memory.
Prerequisites
c.core.6— declarations, definitions, identifiers.
Core Concept
Scope
Scope is the region of program text where an identifier is visible (can be used). It is a *compile-time, lexical* property.
C has four kinds of scope:
| Scope | Where the name is visible |
|---|---|
| File scope | From its declaration to the end of the translation unit (top-level names) |
| Block scope | From its declaration to the end of the enclosing block ({ }) |
| Function prototype scope | Within a function prototype's parameter list |
| Function scope | A label, visible throughout the function (only for labels) |
Storage duration
Storage duration determines how long the storage for an object is reserved. It is a property of the object, not the name.
| Storage duration | Meaning |
|---|---|
| Static | Storage exists for the entire program run |
| Automatic | Storage exists from block entry until block exit |
| Thread | Storage exists for the lifetime of the thread (C11, _Thread_local) |
| Allocated | Storage exists from malloc/calloc/realloc until free (or program end) |
Lifetime
Lifetime is the period during which an object is guaranteed to have its storage and be accessible.
- An object with static storage has lifetime = the whole program run.
- An object with automatic storage has lifetime from entry into its block
until exit from the block (with nuances for VLAs and goto).
- An object with allocated storage has lifetime from the successful
allocation call until free.
- An object with thread storage has lifetime equal to the thread.
The critical rule: accessing an object outside its lifetime is undefined behavior.
Syntax
Automatic storage (the default inside blocks)
void f(void)
{
int x = 1; /* automatic storage: lives until end of f */
}
Static storage
int global_var; /* static storage, external linkage */
static int file_var; /* static storage, internal linkage */
void f(void)
{
static int call_count = 0; /* static storage, block scope */
call_count++;
}
Thread-local storage (C11)
#include <threads.h>
_Thread_local int per_thread_state; /* or thread_local from <threads.h> */
Allocated storage
#include <stdlib.h>
void f(void)
{
int *p = malloc(sizeof *p); /* allocated storage */
if (p != NULL) {
*p = 5;
free(p); /* end of lifetime */
}
}
Examples
Scope vs. lifetime, demonstrated
#include <stdio.h>
int *make_dangling(void)
{
int local = 42; /* automatic: dies at end of make_dangling */
return &local; /* DANGER: returns pointer to dead object */
}
int main(void)
{
int *p = make_dangling();
/* printf("%d\n", *p); /* UNDEFINED BEHAVIOR: use after lifetime */
(void)p;
return 0;
}
This is a classic bug. local is still "in scope" lexically inside the function, but once make_dangling returns, its lifetime ends.
Static local keeps its value
#include <stdio.h>
int counter(void)
{
static int n = 0;
return ++n;
}
int main(void)
{
printf("%d %d %d\n", counter(), counter(), counter());
return 0;
}
Expected output is 1 2 3 (assuming left-to-right evaluation; the order of evaluation of arguments is unspecified in C, but with three calls to the same function the *calls* happen in some order — this example is best understood by calling in separate statements to avoid evaluation-order subtleties). See c.ops.sequencing.
Better:
int a = counter();
int b = counter();
int c = counter();
printf("%d %d %d\n", a, b, c); /* 1 2 3 */
How It Works
- Scope is decided entirely by the compiler while parsing.
- Storage duration decides which memory region backs an object: static
objects go in the data/bss sections (or rodata for const), automatic objects go on the stack, allocated objects come from the heap.
- Lifetime is a semantic guarantee derived from storage duration.
A name going out of scope does not necessarily end an object's lifetime (e.g., a static local remains alive after its block exits). Conversely, an object's lifetime can end while a name still refers to it (e.g., free(p) while p is still in scope).
Variations
VLAs and goto
A variable-length array (c.arr.vla) has automatic storage but its lifetime begins when its declaration is reached, not at block entry. Jumping into the scope of a VLA with goto is a constraint violation in some cases.
Compound literals
A compound literal at block scope has automatic lifetime; at file scope it has static lifetime (c.struct.compound-literal).
Common Mistakes
- Returning a pointer to a local variable.
- Storing the address of an automatic variable in a global and using it after
the function returns.
- Confusing "name is visible" (scope) with "object is alive" (lifetime).
- Assuming
staticlocal means "constant" — it means "static storage," not
"immutable."
Undefined Behavior
- Accessing an object outside its lifetime is undefined behavior.
VERIFIED - Reading an indeterminate value (e.g., an uninitialized automatic variable of
non-unsigned char type) can be UB or an unspecified value (c.obj.indeterminate).
- Using a pointer after
freeis UB (c.mem.dangling).
Portability
- Thread-local storage is C11 and later;
_Thread_localis the keyword,
thread_local is a macro from <threads.h>.
- The exact placement of static objects (data vs. bss) is implementation
detail, but the *guarantee* of zero initialization for static storage is standard.
Under the Hood
- Static objects live in the executable's data/bss segments; they are
initialized before main runs (zero for bss, explicit values for data).
- Automatic objects live on the stack; the stack pointer moves to allocate
and deallocate them at block entry/exit.
- Allocated objects live on the heap, managed by
malloc/free.
This mapping is developed fully in c.memory.1.
Practical Usage
- Prefer returning values or using output parameters over returning pointers to
locals.
- Use
staticlocals for state that must persist across calls but be private
to a function.
- Use allocated storage for objects that must outlive the function that
created them, and establish clear ownership (c.mem.ownership).
Exercises
1. Write a function with a static local and a function with an automatic local; call each several times and explain the difference. 2. Write a function that returns a pointer to a local, then demonstrate the bug by running it (it may "work" or crash — explain why either is possible). 3. For each of these, state scope, storage duration, and lifetime: a file-scope int, a block-scope static int, a block-scope automatic int, and a malloced int.
Deep Challenge
Explain, using the C standard's definitions of scope, storage duration, and lifetime, exactly why this program's behavior is undefined and at what point the undefined behavior occurs:
#include <stdlib.h>
int *g;
void store(void)
{
int x = 7;
g = &x;
}
int main(void)
{
store();
return *g; /* ? */
}
Then rewrite it so the same value is accessible after store returns, in two correct ways (one using static storage, one using allocated storage).
Related Concepts
c.lang.linkage— how names connect across files.c.memory.1— the memory model (where each storage class lives).c.mem.dangling— dangling pointers in depth.c.obj.indeterminate— indeterminate values.
References
- ISO/IEC 9899:2018 §6.2.1 (scope), §6.2.4 (storage durations), §7.22.3
(memory management).
Verification
- The four storage durations (static, automatic, thread, allocated) are
standard. VERIFIED
- Accessing an object outside its lifetime is UB.
VERIFIED - Static storage is zero-initialized if not explicitly initialized.
VERIFIED
- No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Scope (file, block, prototype, function)
- [ ] Storage duration (static, automatic, thread, allocated)
- [ ] Lifetime
- [ ] Scope vs. lifetime distinction
- [ ] Static locals
- [ ] Returning pointers to locals (bug)
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.lang.scope | 0 | 5 |
| c.lang.storage-duration | 0 | 6 |
| c.lang.lifetime | 0 | 6 |