Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

C Variable Scope, Lifetime, and Visibility by Storage Class

Hello! In the previous lesson, you traced function calls by keeping a parameter separate from the caller’s variable. You also saw that a function can change a variable outside itself. To trace such programs reliably, you need to know which variable a name denotes and whether that variable still exists.

This lesson gives you a method for determining a C variable’s scope, storage duration, and linkage. You will apply it to auto, register, static, and extern declarations—the distinctions most likely to matter in GATE-style tracing questions.


Three different questions about a variable

Suppose a function contains a variable called count. “Is count local?” is not quite enough to predict its behavior. Ask three more precise questions:

  1. Scope: At which points in the source code can this declaration’s name be used?
  2. Storage duration: How long does the object exist while the program runs?
  3. Linkage: Can a declaration of the same name in another scope or source file refer to that same object?

Scope concerns the name; storage duration concerns the object. A variable can therefore be visible only inside one function yet retain its value throughout the program.

A source file is processed as a translation unit; for our examples, you can treat each .c file as a separate translation unit. File-scope declarations appear outside functions. A declaration inside { ... } has block scope: its name is available from its declaration onward within that block, except where a nested declaration hides it.

int value = 5;             /* File-scope name. */

void show(void) {
    printf("%d ", value);  /* Refers to the file-scope value: 5. */

    {
        int value = 10;    /* A different object hides the outer name. */
        printf("%d ", value);
    }

    printf("%d\n", value); /* The inner declaration is out of scope. */
}

show() prints 5 10 5. The file-scope object was not destroyed or changed when the inner value was declared; only the name selected inside the nested block changed. Likewise, declaring a local variable with the same name as a file-scope variable does not create a second name for the same object.

Read cppreference’s Storage-class specifiers for the formal rules behind these three questions. Focus on the ordinary C declarations in this lesson; the C23 and thread-local details on the page are not needed for the traces below.

Storage-class specifiers - cppreference.com

Cppreference distinguishes storage duration from linkage, rather than treating a storage-class keyword as a single rule about where a variable “lives.”

In “Explanation,” read the specifier rules, including the defaults near the end. In “Storage duration,” read automatic and static duration; you may skim the later thread and allocated-duration entries. Then, under “Linkage,” read the three linkage categories. Pay particular attention to the fact that a block-scope static variable has no linkage, whereas a file-scope static variable has internal linkage.


Automatic duration versus a persistent local variable

An ordinary variable declared inside a block has automatic storage duration. Entering the block creates a new instance; leaving it ends that instance’s lifetime. Explicit auto normally states this default rather than changing it. Function parameters also have automatic duration.

int next(void) {
    int count = 0;     /* Equivalent storage duration to auto int count = 0; */
    ++count;
    return count;
}

Separate calls to next() each return 1. On every call, a new count is initialized to zero. The fact that storage might be reused between calls does not preserve the old object’s value.

Now change just one declaration:

int next(void) {
    static int count = 0;
    ++count;
    return count;
}

Here count still has block scope: code in another function cannot name it directly. But it has static storage duration: one object exists for the entire program run, and its initialization is performed once, not on every call. Successive calls return 1, 2, and 3.

Watch Neso Academy’s Variable Modifiers – Static for the contrast between file-scope and block-scope uses of static. The lesson’s wording occasionally calls a retained value “immutable”; interpret that as persistent, not unchangeable—the example increments it on every call.

Variable Modifiers − Static

Neso Academy uses a counter to show why static means something different for a name’s visibility at file scope and for an object’s persistence inside a function.

Watch file-scope static for the restriction to one source file, then local static for a counter that retains its value across calls. In the first segment, the missing cross-file definition is described as a compilation fault; distinguish the declaration being accepted during compilation from the unresolved reference reported at linking.

A block-scope static int count; with no explicit initializer starts at zero. The same is true of an uninitialized file-scope int count;: objects with static storage duration receive zero initialization. By contrast:

void f(void) {
    int x;             /* Uninitialized automatic int. */
    /* Do not calculate a numerical output from x here. */
}

x does not receive a guaranteed “garbage number” that you can insert into a trace. Its value is indeterminate; using it as though it held a known integer is not a valid way to solve an output question.

Here is the lifetime distinction inside a loop:

for (int i = 0; i < 3; ++i) {
    int fresh = 0;
    static int retained = 0;

    ++fresh;
    ++retained;
    printf("%d %d\n", fresh, retained);
}

The block is entered on each iteration, so each fresh starts from zero. retained names one persistent object; its initializer does not run anew. The lines printed are 1 1, 1 2, and 1 3.

Finally, register gives a block-scope variable or parameter automatic duration, just like the corresponding ordinary local declaration. It is a hint to the implementation, not a promise that a CPU register will hold the value. One enforceable distinction is that you cannot apply the address-of operator to a register variable, even if the compiler did not actually put it in a register:

register int index = 2;
/* &index is not permitted. */

A register variable left uninitialized has no guaranteed initial value, just like an uninitialized ordinary automatic variable.


File-scope visibility and extern

A variable declared outside functions has file scope and static storage duration. Whether another translation unit can refer to the same object depends on linkage:

int shared = 7;        /* File scope; external linkage. */
static int private = 4;/* File scope; internal linkage. */

Both objects exist throughout execution. Their names can be used later in this source file, subject to ordinary scope and hiding rules. But shared has external linkage, so a compatible declaration in another translation unit can refer to it. private has internal linkage: a declaration in another translation unit cannot refer to this object, even if it spells the name the same way. Thus “file scope” does not mean “automatically visible in every file.”

extern is commonly used to declare an object whose definition is elsewhere. In this two-file example, both uses of shared refer to one object:

/* state.c */
int shared = 7;          /* Definition: supplies the object. */
/* main.c */
#include <stdio.h>

extern int shared;       /* Declaration: refers to that object. */

int main(void) {
    shared += 2;
    printf("%d\n", shared);
    return 0;
}

With both files linked, the program prints 9. extern int shared; does not create a fresh integer initialized to zero. If the program refers to shared but no matching definition is linked, an unresolved-reference error normally arises at the linking stage.

Neso Academy’s Variable Modifiers – Auto & Extern shows this declaration-versus-definition distinction with two source files.

Variable Modifiers − Auto & Extern

Neso Academy demonstrates why an extern declaration can let one file use an object defined in another, and what happens when the definition is missing.

Watch declaration versus definition and cross-file use. In the multi-file demonstration, start at the linked example; focus on the definition in the second file and the linker error after it is removed. Think of the declaration as making a name available at its location, not as automatically making that name visible everywhere in the program.

extern can also appear inside a block:

int total = 6;

void report(void) {
    extern int total;
    printf("%d\n", total);
}

Within report, that declaration makes the name total available in the block while referring to the existing file-scope object; it does not give the object automatic duration. The object still lives throughout the program. More generally, a block-scope extern declaration usually refers to an object with external linkage; if an earlier declaration with internal linkage is visible, it can instead retain that internal linkage.

Keep declaration and definition distinct. A plain file-scope extern int total; is normally only a declaration. At file scope, however, extern int total = 6; includes an initializer and is a definition; it supplies the object. This is why “extern never allocates storage” is too broad a rule.

The common cases can be summarized without conflating scope and lifetime:

Declaration Name’s scope Object’s duration Linkage
Block-scope int x or auto int x Block Automatic None
Block-scope register int x Block Automatic None
Block-scope static int x Block Static None
File-scope int x File Static External
File-scope static int x File Static Internal
Block-scope extern int x Block Static for the object it refers to Usually external

The table describes the usual cases; a declaration’s position still matters for determining which names are available at a particular line. C also has thread and allocated storage durations, but they are not needed to distinguish these four ordinary storage-class specifiers here.


Put the rules together in a trace

In a GATE output question, annotate declarations before calculating values. Consider this single-file program:

#include <stdio.h>

static int offset = 2;

int compute(int n) {
    static int calls;
    ++calls;

    {
        int n = 10 + calls;
        printf("%d ", n);
    }

    return n + calls + offset;
}

int main(void) {
    int first = compute(3);
    int second = compute(3);

    printf("%d %d\n", first, second);
    return 0;
}

There are two different objects named n during each call: the parameter and the int n in the inner block. The inner one hides the parameter only until that block ends. calls is visible within compute but retains its value across calls. offset is visible within this file and also persists, despite the static on its file-scope declaration serving the additional purpose of giving it internal linkage.

On the first call, calls becomes 1, the inner n prints 11, and the returned value is 3 + 1 + 2 = 6. On the second, calls becomes 2, the inner n prints 12, and the return value is 3 + 2 + 2 = 7. The complete output is:

11 12 6 7

Notice why calculating from names alone would fail: the n printed inside the nested block is not the n used by the return statement.


Key takeaways

For each declaration, determine where its name can be used, how long its object exists, and whether another declaration can refer to the same object. Ordinary locals and register variables have automatic duration; block-scope static variables remain local in scope but persist across calls. File-scope variables also persist, while static at file scope restricts linkage to the current translation unit. extern commonly declares an existing object rather than creating another one.

Next, you will apply the automatic-versus-static lifetime distinction to recursive calls, where each call has its own parameters and automatic variables while a local static object remains shared across calls.

Can't find a good explanation? Sign up and we'll make it for you

Sign up