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:
- The return address (where to resume after the function returns).
- The saved frame pointer (to restore the caller's stack frame).
- Function arguments (passed via registers or the stack, depending on the calling convention).
- 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 xhides an outerint x. This compiles but is a common source of bugs. Compile with-Wshadowto 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. Ifncomes from user input, it can overflow the stack. Prefermallocfor 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.
staticon a local variable: persistent storage.staticon a file-scope variable or function: internal linkage (file-private)._Thread_localgives 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
mallocrespectively. - Never return a pointer to an automatic local variable. Use static storage, heap allocation, or caller-provided buffers.