4 min read
On this page

Scope & Lifetime

Scope: Where a Name Is Visible

Scope determines where in the source code a variable name can be used. C has four kinds of scope.

Block Scope

Variables declared inside braces {} are visible only within that block. This includes function bodies, if statements, loops, and standalone blocks.

#include <stdio.h>

int main(void) {
    int x = 10;

    if (x > 5) {
        int y = 20;  /* y is only visible in this block */
        printf("x=%d, y=%d\n", x, y);
    }

    /* printf("y=%d\n", y);  <-- compile error: y is not in scope */

    for (int i = 0; i < 3; i++) {
        int temp = i * 2;  /* temp is created and destroyed each iteration */
        printf("temp=%d\n", temp);
    }

    /* i and temp are not accessible here */

    return 0;
}
x=10, y=20
temp=0
temp=2
temp=4

An inner block can shadow a variable from an outer block:

int x = 10;
{
    int x = 99;  /* shadows outer x */
    printf("inner x: %d\n", x);  /* prints 99 */
}
printf("outer x: %d\n", x);  /* prints 10 */

Shadowing is legal but confusing. Most compilers warn about it with -Wshadow.

File Scope

Variables and functions declared outside any function have file scope. They are visible from the point of declaration to the end of the translation unit.

/* File scope */
static int request_count = 0;

static void increment(void) {
    request_count++;
}

int get_request_count(void) {
    return request_count;
}

The static keyword limits visibility to the current file. Without static, file-scope variables and functions have external linkage and are visible to other translation units.

Function Scope

Only labels (used with goto) have function scope. A label is visible throughout its entire function, regardless of block nesting.

void process(int *data, int n) {
    for (int i = 0; i < n; i++) {
        if (data[i] < 0) goto error;
    }
    return;

error:
    fprintf(stderr, "Negative value found\n");
}

Function Prototype Scope

Parameter names in a function prototype are scoped to the prototype itself. They are documentation only and do not affect the function definition.

/* These parameter names are only for documentation */
void draw_line(int x1, int y1, int x2, int y2);

Storage Duration: How Long a Variable Lives

Automatic Storage (Stack)

Local variables have automatic storage duration. They are created when execution enters their block and destroyed when it exits.

#include <stdio.h>

void count_calls(void) {
    int counter = 0;  /* created fresh every call */
    counter++;
    printf("counter = %d\n", counter);
}

int main(void) {
    count_calls();
    count_calls();
    count_calls();
    return 0;
}
counter = 1
counter = 1
counter = 1

The counter resets to 0 on each call because the variable is destroyed and recreated.

Static Storage

The static keyword on a local variable gives it static storage duration. It persists for the entire program and retains its value between calls.

#include <stdio.h>

void count_calls(void) {
    static int counter = 0;  /* initialized once, persists forever */
    counter++;
    printf("counter = %d\n", counter);
}

int main(void) {
    count_calls();
    count_calls();
    count_calls();
    return 0;
}
counter = 1
counter = 2
counter = 3

Static local variables are initialized once, before main runs. The initialization value must be a compile-time constant.

File-scope variables (global variables) also have static storage duration, whether or not they use the static keyword. The static keyword on a file-scope variable controls linkage (visibility), not lifetime.

Thread-Local Storage

The _Thread_local specifier (C11) gives each thread its own copy of a variable:

#include <stdio.h>
#include <threads.h>

_Thread_local int per_thread_counter = 0;

int thread_func(void *arg) {
    int id = *(int *)arg;
    for (int i = 0; i < 5; i++) {
        per_thread_counter++;
    }
    printf("Thread %d counter: %d\n", id, per_thread_counter);
    return 0;
}

int main(void) {
    thrd_t t1, t2;
    int id1 = 1, id2 = 2;

    thrd_create(&t1, thread_func, &id1);
    thrd_create(&t2, thread_func, &id2);

    thrd_join(t1, NULL);
    thrd_join(t2, NULL);

    return 0;
}
Thread 1 counter: 5
Thread 2 counter: 5

Each thread gets its own per_thread_counter, so both reach 5 independently. Without _Thread_local, they would share the variable and race.

The Stack Frame

When a function is called, the compiler creates a stack frame containing:

  1. The return address (where to resume after the function returns).
  2. The saved frame pointer (to restore the caller's stack frame).
  3. Function arguments (passed via registers or the stack, depending on the calling convention).
  4. Local variables.
void inner(int c) {
    int local_c = c + 1;
    /* Stack: [main frame][outer frame][inner frame]
       inner frame contains: return address, c, local_c */
}

void outer(int a, int b) {
    int local_ab = a + b;
    inner(local_ab);
}

int main(void) {
    outer(3, 4);
    return 0;
}

Each call pushes a new frame. Each return pops one. The stack grows and shrinks automatically.

Stack Overflow

The stack has a fixed size (typically 1-8 MB). Two things cause stack overflow.

Deep Recursion

/* This will crash with a stack overflow for large n */
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

int main(void) {
    printf("%d\n", factorial(1000000));  /* stack overflow */
    return 0;
}

Each recursive call adds a stack frame. A million calls exceed the stack limit.

The fix is to use iteration:

long factorial_iterative(int n) {
    long result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

Large Local Arrays

void process(void) {
    int buffer[10000000];  /* 40 MB on the stack - overflow */
    /* ... */
}

Large arrays should be heap-allocated with malloc instead:

void process(void) {
    int *buffer = malloc(10000000 * sizeof(int));
    if (!buffer) { /* handle error */ return; }
    /* ... */
    free(buffer);
}

Real-World Example: Static Variables for State

Static local variables are useful for one-time initialization and caching:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

const char *get_config_path(void) {
    static char path[512];
    static int initialized = 0;

    if (!initialized) {
        const char *home = getenv("HOME");
        if (home) {
            snprintf(path, sizeof(path), "%s/.config/myapp/config.ini", home);
        } else {
            snprintf(path, sizeof(path), "/etc/myapp/config.ini");
        }
        initialized = 1;
    }

    return path;
}

int main(void) {
    printf("Config: %s\n", get_config_path());
    printf("Config: %s\n", get_config_path());  /* same path, no recomputation */
    return 0;
}

The path is computed once and reused. The static buffer persists between calls, so returning its address is safe (unlike returning a pointer to an automatic local).

Real-World Example: Scope for Resource Management

Block scope is useful for limiting the lifetime of resources:

#include <stdio.h>

int process_file(const char *filename) {
    int result = -1;

    {
        FILE *f = fopen(filename, "r");
        if (!f) return -1;

        char line[256];
        int count = 0;
        while (fgets(line, sizeof(line), f)) {
            count++;
        }

        result = count;
        fclose(f);
    }
    /* f is no longer in scope here - cannot accidentally use it */

    printf("File had %d lines\n", result);
    return result;
}

Common Pitfalls

  • Returning a pointer to a local variable. The variable is destroyed when the function returns, leaving a dangling pointer. Use static, malloc, or caller-provided buffers instead.
  • Shadowing variables. An inner int x hides an outer int x. This compiles but is a common source of bugs. Compile with -Wshadow to catch it.
  • Static local variables are not thread-safe. Multiple threads calling a function with a static local variable will race on it. Guard with a mutex or use _Thread_local.
  • Assuming static means constant. static int x = 0; creates a persistent variable, not a constant. It can be modified.
  • Stack overflow from VLAs. Variable-length arrays (int arr[n]) are allocated on the stack. If n comes from user input, it can overflow the stack. Prefer malloc for variable-size allocations.
  • Uninitialized automatic variables. Local variables without initializers contain garbage. Static variables are zero-initialized by default.

Key Takeaways

  • Block scope limits visibility to the enclosing braces. File scope extends to the end of the translation unit.
  • Automatic (local) variables live on the stack and are destroyed when their block exits. Static variables persist for the program's lifetime.
  • static on a local variable: persistent storage. static on a file-scope variable or function: internal linkage (file-private).
  • _Thread_local gives each thread its own copy of a variable, avoiding data races.
  • The stack has a fixed size. Deep recursion and large local arrays cause stack overflow. Use iteration and malloc respectively.
  • Never return a pointer to an automatic local variable. Use static storage, heap allocation, or caller-provided buffers.