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 static.

Thread

The lifetime of its thread

thread_local (C23; _Thread_local in C11).

Automatic

Entry to exit of the enclosing block

Ordinary local variables and parameters.

Allocated

From malloc until free

malloc/calloc/realloc/aligned_alloc.

flowchart TB Q0([an object is needed]) --> Q1{"must it outlive
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.

counter.c
#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 { … } (including a for/if statement’s own declarations).

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

int x; at file scope

External

A tentative definition; becomes int x = 0; if nothing else defines it.

int x = 1; at file scope

External

A definition. Exactly one per program.

extern int x;

External

A declaration only — the definition is elsewhere. Put this in a header.

static int x; at file scope

Internal

Private to this translation unit.

int x; inside a block

None

An automatic object; no other TU can name it.

static int x; inside a block

None

Static duration, but still no linkage.

void f(void);

External

Function declarations are extern by default.

static void f(void);

Internal

Private to this translation unit.

The header/source split that follows from this table:

registry.h
#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 */
registry.c
#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 0, floating-point to +0.0, pointers to null, aggregates member-wise. Guaranteed, and free — it happens before main.

Thread

Zero-initialized the same way, once per thread as it starts.

Automatic

Indeterminate. Reading it is undefined behavior.

Allocated

Indeterminate from malloc; zeroed from calloc.

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

References