Storage Duration, Scope and Linkage
|
This section documents C23 (ISO/IEC 9899:2024), per ISO/IEC JTC1/SC22/WG14’s freely available working draft N3220, which WG14 documents as differing from the published standard only editorially — the reference these pages are written and verified against. This content was generated with the assistance of AI and should be verified against the WG14 draft and cppreference.com’s C reference before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Three independent properties decide what a declared name means in C, and conflating them is the source of most
confusion around the keyword static:
-
Storage duration — how long the object exists.
-
Scope — where the name is visible.
-
Linkage — whether the name refers to the same object in other translation units.
The Four Storage Durations
| Duration | Lifetime | Declared by |
|---|---|---|
Static |
The whole program run |
File-scope objects, and any object declared |
Thread |
The lifetime of its thread |
|
Automatic |
Entry to exit of the enclosing block |
Ordinary local variables and parameters. |
Allocated |
From |
|
the enclosing block?"} Q1 -->|no| A1["automatic:
a plain local variable"] Q1 -->|yes| Q2{"is its size or count
known at compile time?"} Q2 -->|no| A2["allocated:
malloc / calloc, and one free"] Q2 -->|yes| Q3{"does each thread need
its own copy?"} Q3 -->|yes| A3["thread:
thread_local"] Q3 -->|no| Q4{"should other translation
units see the name?"} Q4 -->|no| A4["static storage,
internal linkage: static at file scope"] Q4 -->|yes| A5["static storage,
external linkage: one definition,
extern declaration in a header"]
Automatic Storage
#include <stdio.h>
static int counter(void)
{
int local = 0; // fresh object on every call, indeterminate without the = 0
++local;
return local; // always 1
}
int main(void)
{
printf("%d %d %d\n", counter(), counter(), counter()); // 1 1 1
for (int i = 0; i < 3; ++i) {
int inner = i * 2; // created and destroyed on each iteration
printf("%d ", inner);
}
putchar('\n');
return 0;
}
An automatic object’s lifetime ends at the closing brace, and any pointer to it is dangling from that moment.
It is not zero-initialized — reading it before writing it is undefined behavior, which -Wuninitialized
and -fsanitize=memory look for.
Variable-length arrays and compound literals also have automatic storage duration, scoped to their block.
Static Storage
static Inside a Function
A local static object keeps its value across calls, is initialized once before the program starts, and is
still only visible inside that function:
#include <stdio.h>
static int next_id(void)
{
static int id = 0; // initialized once, at program start-up
return ++id; // 1, 2, 3, ...
}
static const char *month_name(int index)
{
// A static const table: no repeated construction, and the strings live forever.
static const char *const names[] = {
"January", "February", "March"
};
if (index < 0 || (size_t)index >= sizeof names / sizeof names[0]) {
return "unknown";
}
return names[index];
}
int main(void)
{
printf("%d %d %d %s\n", next_id(), next_id(), next_id(), month_name(1));
return 0;
}
The caveats: a function with local static state is not reentrant and not thread-safe without
synchronization, and returning a pointer to a local static buffer means the next call overwrites the
previous caller’s result (the flaw in strtok, asctime and getenv-style APIs). Use thread_local or an
explicit output parameter instead.
static at File Scope — Internal Linkage
At file scope static means something entirely different: this name is private to this translation unit.
#include <stdio.h>
static int call_count; // internal linkage: invisible to other TUs
int public_total; // external linkage: one definition for the program
static void bump(void) // internal linkage: a private helper
{
++call_count;
++public_total;
}
void run_twice(void); // external linkage: declared in a header
void run_twice(void)
{
bump();
bump();
printf("calls=%d total=%d\n", call_count, public_total);
}
Make every object and function static unless a header declares it. It prevents name collisions at link time,
lets the optimizer see all uses, and documents the module boundary.
Scope
C has four scopes:
| Scope | Extends from the declaration to… |
|---|---|
Block |
The end of the enclosing |
File |
The end of the translation unit. |
Function |
The end of the function — this applies only to labels. |
Function prototype |
The end of the prototype (parameter names in a declaration). |
#include <stdio.h>
int shadowed = 1; // file scope
int main(void)
{
printf("%d\n", shadowed); // 1 -- the file-scope object
int shadowed = 2; // block scope: shadows the outer name
printf("%d\n", shadowed); // 2
{
int inner = 3;
printf("%d %d\n", shadowed, inner); // 2 3
}
// inner is out of scope here
for (int shadowed = 4; shadowed < 5; ++shadowed) {
printf("%d\n", shadowed); // 4 -- the loop's own declaration
}
printf("%d\n", shadowed); // 2 again
return 0;
}
Shadowing is legal and occasionally useful, but far more often a bug — turn on -Wshadow. Declare each
variable in the smallest scope that needs it; C99 lets you declare at first use rather than at the top of the
block.
Linkage
| Declaration | Linkage | Meaning |
|---|---|---|
|
External |
A tentative definition; becomes |
|
External |
A definition. Exactly one per program. |
|
External |
A declaration only — the definition is elsewhere. Put this in a header. |
|
Internal |
Private to this translation unit. |
|
None |
An automatic object; no other TU can name it. |
|
None |
Static duration, but still no linkage. |
|
External |
Function declarations are |
|
Internal |
Private to this translation unit. |
The header/source split that follows from this table:
#ifndef REGISTRY_H
#define REGISTRY_H
#include <stddef.h>
extern size_t registry_size; // DECLARATION -- no storage allocated here
extern const char *const registry_name; // ditto
void registry_reset(void); // extern is implicit for functions
#endif /* REGISTRY_H */
#include "registry.h"
size_t registry_size = 0; // the one DEFINITION
const char *const registry_name = "default"; // the one DEFINITION
static bool initialized; // private to this file
void registry_reset(void)
{
registry_size = 0;
initialized = false;
}
Putting int x; (rather than extern int x;) in a header is the classic mistake: every including translation
unit gets a tentative definition, and modern linkers reject the duplicates (GCC 10+ defaults to
-fno-common).
Initialization Rules per Storage Class
| Storage duration | If you write no initializer |
|---|---|
Static |
Zero-initialized: integers to |
Thread |
Zero-initialized the same way, once per thread as it starts. |
Automatic |
Indeterminate. Reading it is undefined behavior. |
Allocated |
Indeterminate from |
A further restriction: an object with static or thread storage duration must be initialized with a constant expression — you cannot call a function to initialize one:
#include <stdio.h>
#include <time.h>
static int compile_time_ok = 4 * 8; // fine: a constant expression
// static time_t bad = time(nullptr); // error: not a constant expression
static time_t start_time; // zero-initialized...
static void initialize(void)
{
start_time = time(nullptr); // ...then set at run time
}
int main(void)
{
initialize();
printf("%d %d\n", compile_time_ok, start_time != 0);
return 0;
}
This is why C libraries have explicit *_init functions, and why call_once exists — see
Threads.
thread_local
Each thread gets its own copy, created as the thread starts and destroyed as it ends:
#include <stdio.h>
#include <threads.h>
static thread_local int local_counter; // one per thread, zero-initialized
static int shared_counter; // ONE for the whole program -- needs a mutex
static int worker(void *arg)
{
(void)arg;
for (int i = 0; i < 1000; ++i) {
++local_counter; // no synchronization needed
}
printf("thread saw %d\n", local_counter); // 1000, in every thread
return 0;
}
int main(void)
{
thrd_t t1, t2;
if (thrd_create(&t1, worker, nullptr) != thrd_success) {
return 1;
}
if (thrd_create(&t2, worker, nullptr) != thrd_success) {
thrd_join(t1, nullptr);
return 1;
}
thrd_join(t1, nullptr);
thrd_join(t2, nullptr);
printf("main saw %d, shared = %d\n", local_counter, shared_counter); // 0, 0
return 0;
}
thread_local is the C23 keyword; C11 spelled it _Thread_local and provided a thread_local macro in
<threads.h>. It is the clean fix for errno-style per-thread state and for making a function with static
state thread-safe. For dynamically created per-thread values with a destructor, use tss_t — see
Threads.
See Also
-
Program Structure — declarations vs. definitions and the header/source split.
-
Dynamic Memory Allocation — allocated storage duration in depth.
-
Functions —
staticandinlinefunctions. -
Threads —
thread_local,tss_tandcall_once. -
C++: Program Structure and Compilation — C++ keeps these storage durations and adds name mangling,
extern "C"and modules to linkage.
References
-
WG14 N3220 — the C23 working draft (§6.2.1 "Scopes of identifiers", §6.2.2 "Linkages of identifiers", §6.2.4 "Storage durations of objects", §6.7.1 "Storage-class specifiers").
-
cppreference.com — Storage-class specifiers and storage duration.
-
GCC manual — Code Generation Options (
-fcommon/-fno-common).