C Basics · beginner · ~10 min

Scope and lifetime

By the end of this lesson you will be able to: - Distinguish **scope** (where a name is visible) from **lifetime** (when storage exists) and explain why they are different ideas. - Identify **block scope**, **function scope**, and **file scope** in real C code, and predict exactly when each variable is created and destroyed. - Use the `static` keyword in its two distinct meanings: a **persistent local** (keeps its value between calls) and a **file-private global** (hidden from other translation units). - Explain **automatic** vs **static** storage duration and what value an uninitialized variable holds in each case. - Recognize and avoid the classic bug of **returning a pointer to a local variable**, and reason about why globals are shared across threads.

Overview

When you write a program, you give things names: variables, parameters, helper values. Two questions immediately follow every name you create. Where in the source code is this name usable? and How long does the actual memory behind it stay alive while the program runs? The first question is scope; the second is lifetime (also called storage duration). Beginners often blur them together because for a simple local variable they happen to line up — but they are genuinely separate ideas, and the moment you use static, return values, or pointers, the difference starts to matter a lot.

This lesson builds directly on Functions. There you learned that a function has its own body delimited by { }, that parameters carry values in, and that return sends a value out. Scope is what makes that isolation work: a variable named i inside one function has nothing to do with an i inside another. That isolation is not an accident or a convenience — it is a deliberate language rule that lets you write large programs without every name colliding with every other name.

In plain terms: scope is a compile-time concept — the compiler decides, by reading your braces, which names are visible at each point. Lifetime is a run-time concept — it is about real memory being reserved and released as your program executes. Keep that split in your head and the rest of this lesson falls into place: block-scoped locals live on the stack and vanish when their block ends, while file-level and static variables live for the entire run of the program.

Why it matters

Scope and lifetime are not academic trivia — they are where a large share of real C bugs come from.

  • Dangling pointers. If you return the address of a local variable, the storage is gone the instant the function returns. Reading through that pointer is undefined behavior: it may print garbage, crash, or appear to work until it corrupts something later. Understanding lifetime is the only way to avoid this.
  • Accidental sharing. A variable at file scope is visible to every function in the file, and (without static) to every other file in the program. That makes it easy to change state from far away by accident, which produces bugs that are extremely hard to trace.
  • Encapsulation. static at file scope is C's main tool for hiding implementation details. Libraries use it constantly to keep helper functions and internal state private, so callers cannot depend on — or corrupt — them.
  • Correctness under concurrency. Globals and static locals are shared across threads. Knowing this is the difference between code that works and code that races and produces wrong answers under load.

Getting scope and lifetime right is foundational: nearly every later topic (pointers, dynamic memory, headers, modules) assumes you understand which names are visible and how long memory lasts.

Core concepts

1. Scope vs. lifetime: the core distinction

Definition. Scope is the region of source code in which a name can be used to refer to an entity. Lifetime (storage duration) is the span of run time during which the entity's storage is guaranteed to exist.

Plain explanation. Scope answers "can I type this name here and have it mean something?" Lifetime answers "is the memory still there right now?" They usually agree for ordinary locals, but static deliberately breaks the agreement: a static local has narrow scope (only one block) but long lifetime (the whole program).

  SCOPE (compile time)            LIFETIME (run time)
  "where is the name visible?"    "when does the storage exist?"
  -----------------------------   ------------------------------
  decided by { } braces           decided by storage duration
  block / function / file         automatic / static / (dynamic)

Knowledge check: A static int hits = 0; is declared inside one function. Is its scope wide or narrow? Is its lifetime short or long? (Answer: scope is narrow — only that function's body — but lifetime is long — the entire program run.)

2. Block scope and automatic storage

Definition. A variable declared inside a { } block has block scope: it is visible from its declaration to the closing brace of that block. By default it has automatic storage duration: it is created when control reaches the declaration and destroyed when control leaves the block.

How it works internally. Automatic variables live on the call stack. Each function call pushes a stack frame holding that call's locals; returning pops the frame, instantly invalidating those locals.

Stack grows downward as calls nest:

   +------------------+
   | main's frame     |  int n;  (alive whole program if main runs)
   +------------------+
   | next_id's frame  |  int local_id;  (alive only during the call)
   +------------------+  <- top of stack while next_id runs
              (frame popped on return -> local_id storage reused)

When to use / not use. Use block-scoped locals for almost everything — they are automatically cleaned up and impossible to leak. Do not rely on them surviving after the block ends, and never hand out their address to outlive the block.

Common pitfall. An uninitialized automatic variable holds an indeterminate value (garbage), not zero. Reading it before assigning is undefined behavior.

Knowledge check (predict the output): A loop does for (int i = 0; i < 3; i++) { int t = i * i; }. Outside the loop, after it ends, can you print t or i? (Answer: No — both i and t have block scope tied to the loop; their names are not visible after the loop, and the storage is gone.)

3. File scope and external linkage

Definition. A variable or function declared at file level (outside every function) has file scope: it is visible from its declaration to the end of the file. By default it has static storage duration (lives the whole program) and external linkage (its name is exported so other source files can refer to it).

How it works internally. File-scope variables are not on the stack. They live in a fixed region of the program's memory (the data segment, or BSS if zero-initialized) for the entire run. They are zero-initialized by default if you do not give a value.

When to use / not use. Use file-scope variables sparingly for genuinely program-wide state (e.g., a configuration loaded once). Avoid them as a lazy substitute for passing parameters — they create hidden coupling between functions.

Common pitfall. Forgetting that without static, a global's name is exported. Two files that each define int count; at file scope can cause a multiple-definition link error, or silently share one variable, depending on settings.

4. The two meanings of static

static is one keyword with two distinct jobs depending on where you write it:

Where you write static Effect on scope Effect on lifetime Effect on linkage
On a local (inside a function) unchanged — still block scope changed to whole program n/a (locals have no linkage)
On a file-level variable/function unchanged — still file scope unchanged — already whole program changed to internal (name hidden from other files)

Static local — persistent state. A static local keeps its value between calls. It is initialized once, the first time control reaches it, and then survives every return. This is how a function can "remember" across calls without a global.

Call 1: counter() -> count was 0, becomes 1, returns 1
Call 2: counter() -> count is STILL 1 (survived return), becomes 2, returns 2
Call 3: counter() -> count is STILL 2, becomes 3, returns 3

File-private global. static int g_count; at file scope means "this name is private to this file." Other files cannot link to it. This is the standard way to keep module internals hidden, which you will use heavily in Header files.

Common pitfall. Assuming a static local is re-initialized on each call. It is not — static int n = 5; sets n to 5 only on the first arrival, never again.

Knowledge check (explain in your own words): Why is a static local often a cleaner choice than a global for "remember a value between calls"? (Answer: the static local has the same persistence but its name stays private to the function, so no other code can read or corrupt it — narrow scope, long lifetime.)

Syntax notes

// FILE SCOPE -----------------------------------------------------------
int exported_global = 0;        // file scope, program lifetime, EXPORTED name
static int file_private = 0;    // file scope, program lifetime, name hidden

static int helper(int x) {      // 'static' on a function = internal linkage
    return x + 1;               // (callable only from within this file)
}

void demo(int param) {          // 'param' has block scope of the function body
    int local = param;          // BLOCK SCOPE, AUTOMATIC: dies at function end
    static int calls = 0;       // BLOCK SCOPE, STATIC lifetime: survives calls
    calls++;                    // 'calls' remembers its value across calls

    {                           // nested block introduces a new, smaller scope
        int inner = local * 2;  // visible only inside these braces
        (void)inner;            // (silence 'unused' warning for the example)
    }                           // 'inner' name no longer visible here
}

Key rules to remember:

  • A name is visible from its declaration down to the closing } of the enclosing block (or end of file for file scope).
  • An inner declaration with the same name shadows the outer one for the rest of the inner block.
  • static never changes scope — it changes lifetime (on locals) or linkage (on file-level names).

Lesson

Scope vs. lifetime

Two related ideas describe how a variable behaves:

  • Scope is where in the code the variable's name is visible.
  • Lifetime is when the variable's storage exists.

They are not the same thing. Scope is about visibility in the source code; lifetime is about memory at run time.

The three scopes

  • Block scope. A variable declared inside {} is visible only within that block. Its storage is released when the block ends.
  • File scope. A variable declared at file level (outside any function) is visible across the file. Add static to keep it private to that file. Without static, the name is exported and other files can refer to it.
  • Function parameters. A parameter behaves like a local variable of the function. It has block scope inside the function body.

Code examples

#include <stdio.h>

/* File-private state: 'static' hides this name from other source files,
   and gives it program-long lifetime. */
static int g_next_id = 0;

/* Returns a fresh id each call. Uses the file-private global. */
int next_id(void) {
    int local_id = ++g_next_id;  /* local: created here, dies at return */
    return local_id;             /* the VALUE is copied out; storage is freed */
}

/* Demonstrates a static local: 'count' persists between calls,
   initialized to 0 exactly once. */
int call_count(void) {
    static int count = 0;        /* set to 0 only on the first call */
    count++;                     /* survives every return */
    return count;
}

/* Demonstrates block scope and shadowing inside one function. */
void scope_demo(void) {
    int x = 10;                  /* visible for the whole function body */
    printf("outer x = %d\n", x);

    for (int i = 0; i < 2; i++) {/* 'i' is visible only inside the loop */
        int x = i + 100;         /* a NEW x that shadows the outer x */
        printf("  loop i=%d, inner x=%d\n", i, x);
    }                            /* inner x and i both gone here */

    printf("outer x still = %d\n", x);  /* unchanged: shadowing is local */
}

int main(void) {
    printf("id: %d\n", next_id());     /* 1 */
    printf("id: %d\n", next_id());     /* 2 */

    printf("calls: %d\n", call_count());/* 1 */
    printf("calls: %d\n", call_count());/* 2 */
    printf("calls: %d\n", call_count());/* 3 */

    scope_demo();
    return 0;
}

What it does. Three small functions each highlight one idea. next_id shows a file-private global feeding a short-lived local. call_count shows a static local that remembers across calls. scope_demo shows block scope and shadowing: the loop has its own x that does not touch the outer x.

Expected output:

id: 1
id: 2
calls: 1
calls: 2
calls: 3
outer x = 10
  loop i=0, inner x=100
  loop i=1, inner x=101
outer x still = 10

Edge cases to notice. count is initialized once, not per call. The inner x does not modify the outer x — shadowing only changes which name the compiler resolves, not the outer storage. After the loop, neither i nor the inner x exists.

Line by line

We trace the program top to bottom.

  1. static int g_next_id = 0; — at file scope. Storage is reserved for the whole run and zero-initialized before main starts. The static keyword hides the name from other files.
  2. First next_id() call: ++g_next_id raises the global from 0 to 1, copies it into local_id, and returns 1. local_id is destroyed as the function returns — but the value 1 was already copied out.
  3. Second next_id() call: the global is still 1 (it persisted), ++ makes it 2, returns 2.
  4. call_count() first call: control reaches static int count = 0; for the first time, so count becomes 0. Then count++ makes it 1; returns 1.
  5. call_count() second call: the initializer is skipped (it only runs the first time). count is still 1 from before, ++ makes it 2.
  6. call_count() third call: count is 2, ++ makes it 3.
  7. scope_demo(): outer x = 10 is printed. Entering the loop, a brand-new x (the inner one) is created each iteration, shadowing the outer x. After the loop ends, the inner x and i are gone, so the final print shows the untouched outer x = 10.

Trace of the persistent values:

Call g_next_id before g_next_id after returns
next_id #1 0 1 1
next_id #2 1 2 2
Call count before count after returns
call_count #1 0 (init) 1 1
call_count #2 1 2 2
call_count #3 2 3 3

Common mistakes

Mistake 1: Returning a pointer to a local variable

Wrong:

int *make_value(void) {
    int v = 42;
    return &v;        /* BUG: v dies when the function returns */
}

Why it is wrong. v has automatic storage; its stack slot is reclaimed the instant make_value returns. The returned pointer is dangling — it points at memory that no longer belongs to anyone. Dereferencing it is undefined behavior.

Corrected (return by value):

int make_value(void) {
    int v = 42;
    return v;         /* copies the value out safely */
}

If you genuinely need a pointer that outlives the function, allocate on the heap (malloc, covered later) and document who must free it, or use a static local if a single shared instance is acceptable.

How to recognize/prevent. Compilers warn: function returns address of local variable. Treat that warning as an error.

Mistake 2: Expecting a static local to reset each call

Wrong belief:

int bump(void) {
    static int n = 0;
    n++;
    return n;         /* programmer expects this to always return 1 */
}

Why it is wrong. n is initialized to 0 only on the first call. It returns 1, 2, 3, ... — not 1 every time. If you wanted a fresh start each call, drop static and use a plain local.

Mistake 3: Leaning on globals instead of parameters

Wrong:

int g_input;                 /* global used to pass data around */
void process(void) { /* reads g_input */ }

Why it is wrong. Any function can change g_input from anywhere, so behavior depends on hidden global state. Corrected: pass the data explicitly: void process(int input). Narrow scope makes code easier to reason about and test.

Debugging tips

Compiler errors

  • 'name' undeclared (first use in this function) — you used a name outside its scope (e.g., a loop variable after the loop, or a local from another function). Move the declaration up, or pass the value in.
  • redefinition of 'name' / multiple definition of 'name' at link time — a file-scope variable is defined in more than one file. Make it static (file-private) or declare it extern in a header and define it once.

Compiler warnings to never ignore

  • function returns address of local variable — a dangling-pointer bug; fix it.
  • 'x' is used uninitialized — an automatic variable read before assignment; it holds garbage.
  • declaration of 'x' shadows a previous local (with -Wshadow) — often a sign you reused a name by accident.

Runtime / logic symptoms

  • A value that "randomly" changes between calls is often an uninitialized automatic local — print it right after declaration to confirm.
  • A counter that resets when you expected it to persist means you forgot static; one that persists when you expected a reset means you used static by mistake.

Concrete steps. Compile with -Wall -Wextra -Wshadow. For dangling-pointer suspicions, run under a sanitizer: gcc -fsanitize=address file.c then run — it reports use-after-return precisely.

Questions to ask when it does not work. Where exactly is this variable declared? When does its block end? Is its storage automatic or static? Did I take the address of something that outlives the function?

Memory safety

Scope and lifetime are at the heart of C memory safety.

  • Use-after-return. Returning or storing the address of an automatic local creates a pointer to freed stack space. Any later read/write through it is undefined behavior and a frequent source of crashes and security bugs. Return by value, allocate on the heap, or use static storage instead.
  • Uninitialized reads. Automatic variables are not zeroed. Reading one before assignment is undefined behavior; always initialize at declaration when in doubt. (File-scope and static variables are zero-initialized by default.)
  • Shadowing hazards. An inner variable that shadows an outer one can hide bugs where you meant to update the outer value. Compile with -Wshadow to catch accidental shadowing.
  • Lifetime of compound literals and arrays. A local array also dies at block end; returning its address is the same dangling-pointer bug as a scalar.
  • Concurrency. File-scope and static variables are shared by all threads. Concurrent reads and writes without synchronization are data races (undefined behavior). Prefer passing per-thread data as parameters, or protect shared state explicitly when you reach concurrency topics.

Robustness habit. Make scope as narrow as the logic allows: declare variables at first use, inside the smallest block that needs them. Narrow scope shrinks the window for misuse and makes the compiler's lifetime guarantees work for you.

Real-world uses

Concrete uses.

  • ID and handle generators. Allocators, file-handle tables, and event systems hand out monotonically increasing ids using a static or file-scope counter, exactly like next_id above.
  • Library encapsulation. Standard C library implementations and most well-written modules use static at file scope to hide internal helpers and state from callers, exposing only a clean API through a header. You will build this pattern in Header files.
  • One-time initialization. A static local flag (static int initialized = 0;) lets a function set itself up on first use and skip the work afterward.
  • Embedded and OS code. Drivers keep device state in file-scope static variables that persist for the device's lifetime, while keeping that state private to the driver module.

Professional best-practice habits.

Beginner rules:

  • Declare each variable in the smallest scope that works, and initialize it where you declare it.
  • Prefer passing parameters and returning values over reading/writing globals.
  • Never return the address of a local variable.

Advanced habits:

  • Use static at file scope to enforce module boundaries; expose state only through functions.
  • Document the lifetime contract of any pointer a function returns (who owns it, who frees it).
  • Treat shared mutable state (globals, static locals) as a concurrency liability and isolate it.

Practice tasks

Beginner 1 — Spot the scope. Given a function with an outer variable, a loop with its own variable, and a nested block, write out (in comments) which lines each name is visible on and where each one dies. Then add a printf that would fail to compile if placed after the loop, and explain the error. Concepts: block scope, lifetime. Hint: trace each closing brace.

Beginner 2 — Persistent greeting count. Write void greet(const char *name) that prints Hello, <name>! and, using a static local, also prints how many times greet has been called so far (This is greeting #N). Call it three times from main. Expected: the count goes 1, 2, 3. Hint: static int n = 0; then n++.

Intermediate 1 — Global vs. static local. Implement the same running-total behavior twice: once with a file-scope global and once with a static local inside the function. Add a second function in the same file that tries to read the value. Note which version lets the second function touch the state and which does not, and write one sentence on why the static local is safer. Concepts: file scope, static local, encapsulation. Hint: the global is visible to both functions; the static local is not.

Intermediate 2 — Find and fix the dangling pointer. You are given a function char *label(int n) that fills a local char buf[16] with text and returns buf. The caller prints garbage. Diagnose why, then produce two safe fixes: (a) have the caller pass in a buffer, and (b) use a static buffer, noting the trade-off of (b). Concepts: automatic lifetime, dangling pointer. Hint: where does buf live after return?

Challenge — Mini module with private state. Design a tiny "ticket dispenser" in one source file: a public int take_ticket(void) that returns the next ticket number starting at 1, and a public int tickets_issued(void) that reports how many have been taken. Keep the counter as a file-private static variable so no other file can reach it. Add a separate main that takes several tickets and prints the running count. Explain, in comments, why static at file scope is the right choice here. Concepts: file scope, static linkage, encapsulation, lifetime. Constraint: no globals with external linkage.

Summary

  • Scope (compile-time) is where a name is visible; lifetime / storage duration (run-time) is when its storage exists. They are separate ideas.
  • Block scope + automatic storage: locals live inside their { } and die when the block ends; they are not zero-initialized.
  • File scope + static storage: file-level names live the whole program and are zero-initialized; without static their names are exported to other files.
  • static has two jobs: on a local it gives persistent lifetime (remembers across calls, initialized once) while keeping narrow scope; on a file-level name it gives internal linkage (private to the file).
  • Most common mistakes: returning a pointer to a local (dangling pointer), expecting a static local to reset each call, reading an uninitialized automatic, and overusing globals.
  • Remember: keep scope as narrow as possible, initialize at declaration, never return the address of a local, and treat globals and static locals as shared (and thus a concurrency hazard).

Practice with these exercises