---
title: 'Variables, Scope and Storage Duration'
source: 'https://academia.sh/en/courses/c-fundamentals/variables-scope-and-lifetime'
course: 'C Fundamentals'
language: en
updated: '2026-09-26T03:02:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "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'
---

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

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.

```c
// 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.

```c
// 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.

```c
// 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.

```c
// 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
```

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

int unit_counter = 2;
```

```c
// 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.

```c
// 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;
}
```

```c
// unit_b.c — carries the same name, but a separate object
static int scope_counter = 22;

int read_from_b(void) {
    return scope_counter;
}
```

```c
// 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.
