Skip to content
academia.sh

Lesson 07 / 13

Variables, Scope and Storage Duration

Where a name is visible from being decided at compile time, and how long a value lives being decided at runtime; whether the same name falls into two separate objects in two units being resolved by the linker.

Published Updated

In this lesson

  • Distinguishes that a name's scope is settled at compile time and a value's storage duration at runtime
  • Shows with an example which stage the static and extern specifiers affect
  • Measures through the linker that the same name can fall into two separate objects in two units
Contents

The Data Types lesson measured how the type a name already carries is decided. This lesson takes a step back: from where is a name itself visible, and how long does the value that name points to live? These two questions look alike, but they are not answered at the same stage — one belongs to the source’s structure, the other to the flow of the run. A third question will be added to these: when a program is built from several units, whether the same name falls into the same object or into two separate objects across two units is also resolved at a separate stage, by a separate rule.

The Programming Fundamentals course (M01/K02) already established the concepts of scope and lifetime with Python; there, a name disappeared once a function call ended, while a module-level name lived until the program’s end. C’s counterpart is measured here: scope and storage duration in C are separated not only conceptually but also in terms of the stage at which they are decided — one is fixed at compile time, the other can only be observed during a run.

Scope: Where a Name Is Visible From

Which declaration a name is bound to is determined at compile time by looking at the source’s structure. When the same name is redeclared in nested blocks, the innermost declaration shadows the outer one — the outer name becomes temporarily inaccessible, but it does not disappear.

// scope.c — which declaration a name is bound to is settled at compile time
#include <stdio.h>

int main(void) {
    int value = 10;
    printf("%s %d\n", "outer value", value);
    {
        int value = 20;
        printf("%s %d\n", "inner value", value);
    }
    printf("%s %d\n", "value after the block", value);
    return 0;
}
outer value 10
inner value 20
value after the block 10

BS3. The value declaration inside the block only shadows the outer value within the boundaries of its own block; once the block closes, the outer name becomes visible again and its value has never changed — because the two value variables are actually two separate objects, two different storage locations carrying the same name. Which value is accessed is determined not by which line the program is on, but by which block that line is written inside in the source; this information is written in full in the source itself, and the compiler resolves it before a run ever starts. The same rule applies to function parameters as well: a function’s parameter name is a name declared in the outermost block of the function body and is subject to the same shadowing rule.

The scope rule is not limited to blocks opened with { }; a counter declared in a for loop’s header also carries its own scope, and once the loop ends that name is no longer visible.

// loop_scope.c — the declaration in a for header belongs to the loop's own scope
#include <stdio.h>

int main(void) {
    int i = 100;
    for (int i = 0; i < 3; i++) {
        printf("%s %d\n", "i inside the loop", i);
    }
    printf("%s %d\n", "i outside the loop", i);
    return 0;
}
i inside the loop 0
i inside the loop 1
i inside the loop 2
i outside the loop 100

The i in the for header shadows the outer-scope i only for the duration of the loop body; once the loop ends, the outer i — untouched, at 100 — becomes visible again. This is a special form of block shadowing: the loop header opens a scope of its own, and this scope is determined solely from how the source is written, without looking at any data.

Storage Duration: How Long a Value Lives

Unlike scope, how long a value lives is observed not by reading the source but by running the program. The measurement below calls the same function three times and counts how two different declaration forms behave across calls.

// duration.c — how long a value lives can only be told by running the program
#include <stdio.h>

int automatic_counter(void) {
    int counter = 0;
    counter++;
    return counter;
}

int persistent_counter(void) {
    static int counter = 0;
    counter++;
    return counter;
}

int main(void) {
    for (int i = 0; i < 3; i++) {
        printf("%s %d\n", "call", i + 1);
        printf("  %s %d\n", "automatic counter", automatic_counter());
        printf("  %s %d\n", "persistent counter", persistent_counter());
    }
    return 0;
}
call 1
  automatic counter 1
  persistent counter 1
call 2
  automatic counter 1
  persistent counter 2
call 3
  automatic counter 1
  persistent counter 3

The counter inside automatic_counter is reborn on every call and dies when the function returns — this is C’s counterpart of the storage duration concept, and it is not confused with M10/K02’s lifetime term: what was measured there was when a value becomes invalid in an ownership chain; what is measured here is how long a variable stays allocated in memory. The static specifier changes this duration: the counter inside persistent_counter is initialized once at the start of the program and does not disappear each time the function returns — the next call finds the value the previous call left behind. Across three calls, the automatic counter always stays at 1 while the persistent counter advances 1, 2, 3; this difference does not show up in the source, it shows up in how many times the function was called — it cannot be told in advance by reading the source, only a run reveals it.

BS4. The static specifier does not affect two different things at once here: counter’s scope is still limited to the function body — it cannot be accessed directly from outside — but its storage duration is now tied not to the function call but to the whole program. That scope and storage duration are separate questions shows up exactly here: in the same declaration, one can change while the other stays fixed.

A variable declared at a file’s outermost level (outside any function body) already lives for the whole program regardless — a file-scope name like unit_counter is permanent in storage duration from the very start, whether or not static is written. What the static specifier adds at file level is not duration but a narrowing of scope: as will be measured below in the scope_counter example, it is closed off outside its own unit. In a local variable, on the other hand, static’s effect works in the opposite direction — it leaves the scope unchanged while extending its duration. The same keyword affects two different decisions depending on where the declaration sits; this is a sign that static does not have a single meaning, and can change either scope or storage duration depending on its context.

The Independence of Scope and Storage Duration

Because scope and storage duration are separate questions, a single declaration can carry different combinations of the two. A local variable (block scope, automatic duration) and a local static variable (block scope, persistent duration) were measured above — both are confined to the same block, but one is reborn on every call while the other persists. A variable declared at file level shows the third combination: unit_counter is file-scoped — visible from everywhere in the unit it is declared in — and its duration is permanent from the start, whether or not static is written. The one combination that does not appear in the source is file scope paired with automatic duration: a name declared at file level never comes to life and dies bound to a function call, because a file-scope declaration is never inside a call frame to begin with. These three combinations show that scope and storage duration really are two independent axes: one answers “where is it visible from,” the other “how long does it live,” and a declaration’s position on these two axes can be chosen separately.

The Linker: A Variable in Another Unit

Scope and storage duration were questions that stayed within a single unit. A third question arises once several units come into play: if a name is defined in another unit, when is access to it resolved? Answering this question requires separating declaration from definition. A declaration only tells the compiler a name’s type and that it exists — it allocates no memory. A definition, on the other hand, actually creates a storage location for that name. extern int unit_counter; is a declaration; int unit_counter = 2; is a definition. A name can be declared as many times as wanted across different units — each unit keeps its own copy — but for a name with external linkage, exactly one definition must exist; otherwise the linker runs into the contradiction of allocating more than one storage location for the same name. The excerpt below is part of the core introduced in the Compilation Stages lesson.

// stage.h — declarations of names defined in other units (excerpt: variable declaration)
#ifndef STAGE_H
#define STAGE_H

/* This variable is defined in another unit: its name is declared here, its value is resolved at link time. */
extern int unit_counter;

#endif
// choice.c — the second unit: the variable's definition lives here
#include "stage.h"

int unit_counter = 2;
// stage.c — the linking of a variable in another unit (excerpt)
#include <stdio.h>
#include "stage.h"

int main(void) {
    printf("%s %d\n", "variable in another unit", unit_counter);
    return 0;
}
variable in another unit 2

stage.c knows the name unit_counter only from its declaration (extern int unit_counter;); this declaration guarantees the name’s type and existence, but not its value. The value is defined in choice.c, and while stage.c is compiled it is not yet bound to anything — the two units are translated separately. The step that finds the name’s real location and binds the reference to it is the linker: when the two units’ object files are brought together, the gap stage.c leaves is filled by the definition choice.c supplies. This is a third decision separate from scope and storage duration: scope determines a name’s visibility, storage duration a value’s span, and binding determines which definition a name corresponds to.

This separation between declaration and definition also explains why header files carry only declarations: stage.h can be included in more than one unit (both can write #include "stage.h"), and each inclusion adds only a declaration, not a definition. If the header file carried a definition like int unit_counter = 2;, every unit including it would produce its own definition, and the linker would end up with more than one storage location for the same name. Separating declaration from definition is therefore not merely a syntactic detail but a precondition for a multi-unit program being compilable at all.

The Bounding Measurement

A name being written the same way in two units does not mean that name corresponds to a single object. An extern variable like unit_counter was deliberately designed to be shared; but a static variable guarantees the exact opposite — it closes itself into its own unit, and no other unit can access it directly. This means the same name can fall into two separate objects in two separate units, and it is the linker that decides this.

// unit_a.c — a scope counter defined in this unit, unreachable from outside
static int scope_counter = 11;

int read_from_a(void) {
    return scope_counter;
}
// unit_b.c — carries the same name, but a separate object
static int scope_counter = 22;

int read_from_b(void) {
    return scope_counter;
}
// bounding.c — the same name is two separate objects in two units, the linker decides
#include <stdio.h>

int read_from_a(void);
int read_from_b(void);

int main(void) {
    printf("%s %d\n", "read from unit a", read_from_a());
    printf("%s %d\n", "read from unit b", read_from_b());
    return 0;
}
read from unit a 11
read from unit b 22

unit_a.c and unit_b.c both define a variable named scope_counter, but the static specifier closes both into their own unit. The linker never compares these two definitions and reports no conflict — because neither ever leaves its own unit, neither appears in the symbol table. read_from_a can see only its own unit’s scope_counter, read_from_b only its own, and the results confirm this: 11 and 22, entirely independent of each other. This is the exact opposite of an extern variable’s behavior: there, a single definition existed and the linker bound every call to that one definition; here, two separate definitions exist and the linker leaves each in its own unit without ever comparing them. Depending on the static specifier, the same name can lead to two completely different outcomes from a linker decision.

The practical consequence of this third stage is that static can be used at file level as a hiding tool. If a unit wants to keep a counter or a helper variable for its own internal working and does not want any other unit to see it, it declares that variable static; even if the name collides with the rest of the units — as in the scope_counter example — the linker reports no conflict, because both stay within the boundaries of their own unit. An extern variable, by contrast, sets up the exact opposite contract: a name deliberately shared between units, with a single definition. The source writer decides which name is to be shared and which is to stay in its own unit through the presence or absence of the static keyword — and this choice has already predetermined the rule the linker will follow when the units are linked.

Summary

  • A name’s scope — where it is visible from — is determined at compile time by looking at the source’s block structure; in nested blocks, the same name shadows the outer one.
  • A value’s storage duration — how long it lives — can only be observed at runtime; an automatic local variable is reborn on every call, a static local variable persists across calls.
  • Storage duration here is C’s counterpart of storage duration; it is kept separate from M10/K02’s lifetime term — one measures the span of allocation, the other the validity of ownership.
  • An extern variable is bound to a single definition and shared across units; this binding is resolved at the linker stage, when the units come together.
  • When the same name is defined static in two units, the linker leaves them as two separate objects without ever comparing them — because scope, storage duration and binding are three separate questions, all three can be decided at once, yet independently of each other.
  • Scope and storage duration are independent axes: a block-scoped name can have automatic or persistent duration, while a file-scoped name always has persistent duration — the combination of file scope with automatic duration never appears in the source.

Next Step

None of the decisions measured in this lesson touched on what determines an expression’s result — only where a name is visible from, how long a value lives, and which unit a definition sits in were measured. The Operators lesson adds a new question to this ground: the same operator can produce a different result depending on the type of the values it operates on, and this choice, too, is made not by looking at data but at the types known at compile time. Even though 7 / 2 and 7 / 2.0 are written with the same operator, which division runs comes from the operands’ types — just as, in this lesson, which declaration a name is bound to comes from the source’s structure.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close