ZetCode

C scope and storage classes

last modified November 14, 2026

Three related ideas decide what a name refers to and how long an object lives. Scope is the part of the source in which a name is visible. Storage duration is the part of the program's execution in which the object exists. Linkage says whether another translation unit can refer to the same name. C expresses these properties with the storage-class specifiers auto, static, extern and register, and with the place where a declaration appears. This tutorial covers block and file scope, shadowing, automatic and static lifetime, external declarations and the register hint.

Block scope

A declaration inside a block is visible from that declaration to the closing brace of the block. A nested block may declare another object with the same name; the inner declaration hides the outer one until the inner block ends, and then the outer name is visible again. The two objects are distinct even though they share a spelling, so changing the inner one does not change the outer one. The next program declares value three times in nested blocks.

scope_blocks.c
#include <stdio.h>

int main(void) {

    int value = 10;
    printf("outer value: %d\n", value);

    {
        int value = 20;
        printf("inner value: %d\n", value);

        {
            int value = 30;
            printf("innermost value: %d\n", value);
        }

        printf("inner value again: %d\n", value);
    }

    printf("outer value again: %d\n", value);

    return 0;
}

Each pair of braces introduces a new scope. The inner declarations are new objects initialised to 20 and 30, and the print after the innermost block sees the middle object again. When the outer block ends, the original object with the value 10 is visible once more. Shadowing is legal, but a long function with several objects of the same name quickly becomes hard to read.

$ gcc -std=c17 -Wall -Wextra -o scope_blocks scope_blocks.c
$ ./scope_blocks
outer value: 10
inner value: 20
innermost value: 30
inner value again: 20
outer value again: 10

Loop scope and shadowing

A declaration in the first clause of a for statement has the loop as its scope. The variable exists from its declaration to the end of the loop body and is destroyed when the loop finishes. An outer variable with the same name is not modified by the loop; the loop declaration simply hides it. This form became available in C99, and it keeps the counter from leaking into the rest of the function.

scope_loop.c
#include <stdio.h>

int main(void) {

    int i = 100;

    printf("before loop: i = %d\n", i);

    for (int i = 0; i < 3; i++) {
        printf("loop i = %d\n", i);
    }

    printf("after loop: i = %d\n", i);

    return 0;
}

The outer i is 100 before the loop and is still 100 afterwards. Inside the loop, the declaration int i = 0 creates a different object whose values are 0, 1 and 2. The compiler option -Wshadow reports such hiding when it is enabled; it is not part of -Wall -Wextra because shadowing is sometimes intentional.

$ gcc -std=c17 -Wall -Wextra -o scope_loop scope_loop.c
$ ./scope_loop
before loop: i = 100
loop i = 0
loop i = 1
loop i = 2
after loop: i = 100

Automatic storage duration

A local variable without static has automatic storage duration. It comes into existence on entry to the block and is initialised when execution reaches its declaration; it is destroyed when the block exits. auto is the storage-class specifier that names this default, but it is almost never written because it adds nothing. An automatic object with no initialiser has an indeterminate value, so it must be assigned before it is read.

storage_auto.c
#include <stdio.h>

void show_auto(void) {

    auto int value = 0;

    value++;
    printf("automatic value: %d\n", value);
}

int main(void) {

    show_auto();
    show_auto();
    show_auto();

    return 0;
}

Each call to show_auto creates a new value, sets it to zero and increments it. The printed result is therefore 1 every time, not 1, 2, 3. The auto int spelling is equivalent to int; the keyword remains in the language mainly for historical symmetry with static and extern.

$ gcc -std=c17 -Wall -Wextra -o storage_auto storage_auto.c
$ ./storage_auto
automatic value: 1
automatic value: 1
automatic value: 1

Static local variables

Adding static to a local declaration changes its storage duration but not its scope. The object is created once, before main starts, and lives until the program ends; the name is still visible only inside its block. An initialiser on a static object is evaluated only once, and an uninitialised static object starts at zero. The next function uses a static counter to remember how many times it has been called.

storage_static_local.c
#include <stdio.h>

void tick(void) {

    static int calls = 0;

    calls++;
    printf("call %d\n", calls);
}

int main(void) {

    tick();
    tick();
    tick();

    return 0;
}

The first call initialises calls to zero, increments it and prints 1. The second and third calls reuse the same object, so the output continues with 2 and 3. A static local is not a global: code outside the function cannot name it, even though the object is alive for the whole run. It is also shared by all calls, so it is not a safe per-thread counter in a multithreaded program.

$ gcc -std=c17 -Wall -Wextra -o storage_static_local storage_static_local.c
$ ./storage_static_local
call 1
call 2
call 3

File scope and internal linkage

A declaration outside every function has file scope: it is visible from that point to the end of the translation unit. Adding static to such a declaration gives the object or function internal linkage, which means other translation units cannot refer to it. The variable and the helper below are therefore private to this source file. A static object at file scope is zero-initialised before the program starts, like any other object with static storage duration.

storage_static_file.c
#include <stdio.h>

static int counter = 0;

static void bump(void) {
    counter++;
}

int main(void) {

    bump();
    bump();

    printf("counter: %d\n", counter);

    return 0;
}

Both counter and bump have file scope and internal linkage. main can use them because it is in the same translation unit; a second .c file compiled into the same program could not call bump or read counter. The two calls increment the counter to 2. Without static, the names would have external linkage and could clash with names in other files.

$ gcc -std=c17 -Wall -Wextra -o storage_static_file storage_static_file.c
$ ./storage_static_file
counter: 2

External declarations

The extern specifier declares an object without defining it. It tells the compiler the name and type and leaves the storage to a definition elsewhere, either later in the same file or in another translation unit. The definition is the declaration that actually allocates the object, such as int shared = 10;. A program may have many declarations of the same external object but only one definition.

storage_extern.c
#include <stdio.h>

extern int shared;

void add(int n) {
    shared += n;
}

int shared = 10;

int main(void) {

    printf("shared: %d\n", shared);

    add(5);

    printf("shared: %d\n", shared);

    return 0;
}

extern int shared; lets add use the variable before its definition appears. The definition int shared = 10; allocates the object and gives it the initial value. After one call to add(5) the value is 15. The same mechanism connects files in a larger program; the C header files tutorial puts the declaration in a header so every source file sees it.

$ gcc -std=c17 -Wall -Wextra -o storage_extern storage_extern.c
$ ./storage_extern
shared: 10
shared: 15

The register specifier

register asks the compiler to keep an object in a processor register if possible. It is only a hint: a modern optimising compiler decides where values live and may ignore the request entirely. A register object cannot have its address taken, so &i is a compile error. The keyword can appear only on a block-scope declaration or on a function parameter, and it is rarely useful in code written for current compilers.

storage_register.c
#include <stdio.h>

int main(void) {

    register int i;
    int sum = 0;

    for (i = 1; i <= 5; i++) {
        sum += i;
    }

    printf("sum: %d\n", sum);

    return 0;
}

The loop variable is declared once before the loop and reused by the for statement. The hint does not change the result: the five integers add up to 15. If the program needed a pointer to i, the declaration would have to drop register and the compiler would be free to place the object in memory.

$ gcc -std=c17 -Wall -Wextra -o storage_register storage_register.c
$ ./storage_register
sum: 15

Source

cppreference: scope
cppreference: storage duration and linkage
cppreference: storage-class specifiers
cppreference: extern

This tutorial covered scope and storage classes in C: block scope and shadowing, the scope of a declaration in a for header, automatic and static storage duration, file scope with internal linkage, extern declarations and the register hint. The C functions tutorial uses these rules for parameters and local variables, the C arrays tutorial shows how automatic arrays are created, and the C header files tutorial puts external declarations in a shared header.

Author

My name is Jan Bodnar, and I'm a dedicated programmer with a deep passion for coding. Since 2007, I've been sharing my expertise through over 1,400 articles and 8 e-books. With more than a decade of teaching experience, I strive to make programming accessible and engaging.

List all C tutorials.