Scope, Lifetime, and Program Memory Layout
Two different questions get confused constantly: where a name can be seen, and how long the object it names exists. C answers them separately, and the answers together explain the whole memory map of a running program.
- Distinguish scope from storage duration and give an example where they differ.
- Explain what a
staticlocal is and when it is the right tool. - Name the six regions of a running program's memory and say what lives in each.
- Describe a stack frame and explain why locals do not survive a return.
- Write a program that prints its own memory map.
1Scope: where a name is visible
| Scope | Extent | Introduced by |
|---|---|---|
| Block | From the declaration to the closing brace | Any declaration inside { } |
| Function | The whole function body | Only labels, for goto |
| File | From the declaration to the end of the file | Declarations outside any function |
| Function prototype | Within the parameter list | Parameter names in a declaration |
int global = 1; /* file scope: visible below, in this file */
void demo(void)
{
int local = 2; /* block scope: this function only */
{
int inner = 3; /* block scope: these braces only */
printf("%d\n", inner);
}
/* inner does not exist here */
}Declare names in the smallest scope that works. A variable visible only where it is used cannot be accidentally read or written elsewhere, and the reader does not have to search for other uses.
Shadowing
int count = 10;
void demo(void)
{
int count = 20; /* shadows the file-scope count */
printf("%d\n", count); /* 20 — the inner one wins */
}An inner declaration hides an outer one of the same name. This is legal and occasionally intentional, but far more often it is an accident that produces a bug the compiler will not mention unless asked. Turn it on:
gcc -Wshadow …-Wshadow is not in -Wall or -Wextra. It is worth adding to your standard command line.
2Storage duration: how long an object lives
| Duration | Lives | Declared as |
|---|---|---|
| Automatic | From entry to its block until exit | A plain local variable |
| Static | The whole program run | static, or any file-scope variable |
| Allocated | From malloc until free | Week 19 |
| Thread | The lifetime of one thread | _Thread_local — week 41 |
Scope and duration are independent, which is the point of separating them. A static local has block scope — only its function can name it — and static duration — it exists for the whole run and keeps its value between calls.
void counter(void)
{
static int calls = 0; /* initialized once, at program start */
calls++;
printf("call number %d\n", calls);
}
/* 1, 2, 3, … across separate calls */Two guarantees worth knowing. A static variable's initializer must be a constant expression and runs once, before main. And any static object without an initializer is zero-initialized — unlike an automatic variable, which contains garbage.
When a static local is right
For a counter or cache private to one function, it is exactly the tool: file-scope visibility would expose it unnecessarily. The cost is that the function is no longer reentrant — two threads calling it share the variable, and so do two nested calls. strtok from week 14 is the standard library's example of this design and its consequences.
static at file scope means something else
static int helper_count; /* not visible to other .c files */
static void helper(void) { … } /* likewise */Applied to a file-scope name, static restricts linkage rather than duration — the name cannot be referenced from another translation unit. That is week 28's subject, and it is the basis of information hiding in C.
3The memory map of a running program
When the operating system loads your executable, it lays out several distinct regions. Every object you have declared so far lives in one of them.
The classic layout. Week 52 inspects the same sections in a firmware image with objdump.
| Region | Holds | Writable | Lifetime |
|---|---|---|---|
.text | Machine code | No | Whole run |
.rodata | String literals, const data | No | Whole run |
.data | Statics with a non-zero initializer | Yes | Whole run |
.bss | Statics initialized to zero | Yes | Whole run |
| Heap | malloc'd blocks | Yes | Until free |
| Stack | Locals, parameters, return addresses | Yes | Until the function returns |
.bss deserves a note. Zero-initialized statics are not stored in the executable file at all — only their total size is recorded, and the loader zeroes that much memory at startup. A program with a ten-megabyte zeroed array has a tiny executable. Week 52 makes this visible with the size command.
This map also explains week 14's crash. A string literal lives in .rodata, which the hardware marks read-only; writing there is refused by the memory management unit, not by C.
4The call stack
Each function call pushes a stack frame containing its parameters, its local variables, and the address to return to. The frame is popped on return.
void inner(int x)
{
int local = x * 2; /* lives in inner's frame */
printf("%d\n", local);
} /* frame popped here */
void outer(void)
{
int value = 21; /* lives in outer's frame */
inner(value); /* inner's frame sits above outer's */
}Three consequences, all of which you have already met.
Locals do not survive the return — week 11's rule and week 13's dangling pointer. The memory is not erased; it simply belongs to the next call, which is why a use-after-return often appears to work.
Recursion consumes stack. Each call is another frame. Week 18 measures how many fit.
The stack is small and fixed. Typically 8 MB on Linux, 1 MB on Windows. A large local array can exhaust it on its own:
void reckless(void)
{
int huge[4000000]; /* 16 MB on the stack — crashes */
huge[0] = 1;
}There is no error message for this, only a segmentation fault. Anything large belongs on the heap, which is week 19. Check your limit with ulimit -s.
Stack overflow is not reported gracefully. The stack grows downward into an unmapped guard page and the program receives a fatal signal. There is no NULL to check and no error code. Sanitizers detect it — -fsanitize=address reports stack-overflow — but the language itself gives you nothing.
5The cost of global state
A file-scope variable is visible to every function below it, which sounds convenient and is the reason it is overused.
int total = 0; /* everyone can read and write this */
void add(int n) { total += n; }
void reset(void) { total = 0; }
int report(void) { return total; }What it costs:
- Reasoning. To know what
reportreturns you must know every call toaddandresetthat has happened. A function taking parameters and returning a value can be understood alone. - Testing. Tests must reset the state between cases, and they cannot run in parallel. Week 35 hits this immediately.
- Reuse. There is exactly one
total. Handling two independent sums requires rewriting all three functions. - Threads. Two threads calling
addconcurrently corrupt the value. Week 41.
The alternative is to pass state explicitly — a parameter now, a pointer to a structure from week 21 onward:
typedef struct { int total; } Accumulator;
void add(Accumulator *a, int n) { a->total += n; }
void reset(Accumulator *a) { a->total = 0; }
int report(const Accumulator *a) { return a->total; }Longer to write and better in every other respect: independent instances, testable in isolation, thread-safe by construction, and the const says which operations are read-only. Week 33 makes this an opaque handle.
Where a genuinely global resource exists — a log file, a configuration loaded once — make it static at file scope so at least the name does not leak into other translation units.
6Worked example: a program that prints its own memory map
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* .data — initialized, writable, whole-run lifetime */
int global_initialized = 42;
double global_double = 3.14;
/* .bss — zero-initialized; costs no space in the executable */
int global_zero;
int big_zero_array[100000];
/* .rodata — read-only */
const char *const message = "stored in read-only memory";
const int lookup[4] = { 1, 2, 3, 4 };
/* file scope but not exported: week 28 */
static int file_private = 7;
/* A static local: block scope, static duration. */
static int call_count(void)
{
static int calls = 0; /* initialized once, before main */
return ++calls;
}
/* Demonstrates that each frame is distinct and reused. */
static void frame_depth(int depth)
{
int marker = depth;
printf(" depth %d: &marker = %p\n", depth, (void *)&marker);
if (depth < 3) {
frame_depth(depth + 1);
}
}
static void shadowing_demo(void)
{
int file_private = 999; /* shadows the file-scope one */
printf(" inside the function, file_private = %d\n", file_private);
printf(" (the file-scope one is still 7, but unreachable by name here)\n");
}
int main(void)
{
int local = 1;
char local_array[64] = "on the stack";
void *heap_block = malloc(1024);
if (heap_block == NULL) {
fprintf(stderr, "allocation failed\n");
return EXIT_FAILURE;
}
puts("== where things live (addresses vary between runs) ==\n");
printf(" %-26s %p\n", ".text (code: main)", (void *)(void *)main);
printf(" %-26s %p\n", ".rodata (string literal)", (void *)message);
printf(" %-26s %p\n", ".rodata (const array)", (void *)lookup);
printf(" %-26s %p\n", ".data (global = 42)", (void *)&global_initialized);
printf(" %-26s %p\n", ".data (static file var)", (void *)&file_private);
printf(" %-26s %p\n", ".bss (global zero)", (void *)&global_zero);
printf(" %-26s %p\n", ".bss (big zero array)", (void *)big_zero_array);
printf(" %-26s %p\n", "heap (malloc 1024)", heap_block);
printf(" %-26s %p\n", "stack (local int)", (void *)&local);
printf(" %-26s %p\n", "stack (local array)", (void *)local_array);
puts("\n read the addresses: code and constants lowest, then the");
puts(" writable statics, then the heap, with the stack far above");
puts("\n== automatic versus static duration ==");
printf(" call_count() -> %d\n", call_count());
printf(" call_count() -> %d\n", call_count());
printf(" call_count() -> %d (the value persisted between calls)\n",
call_count());
puts("\n== zero-initialization ==");
printf(" global_zero (static, no initializer) = %d -- guaranteed 0\n",
global_zero);
puts(" an uninitialized LOCAL would hold garbage instead");
puts("\n== stack frames ==");
printf(" main's local is at %p\n", (void *)&local);
frame_depth(1);
puts(" each frame sits at a lower address; they are popped on return");
puts("\n== scope and shadowing ==");
printf(" at file scope, file_private = %d\n", file_private);
shadowing_demo();
puts("\n== read-only memory ==");
printf(" message = \"%s\"\n", message);
puts(" writing through it would be refused by the hardware");
free(heap_block);
return EXIT_SUCCESS;
}gcc -std=c17 -Wall -Wextra -Wshadow -g -o layout layout.c
./layoutWhat the compiler adds
Because -Wshadow is on:
layout.c:52:9: warning: declaration of 'file_private' shadows
a global declaration [-Wshadow]That warning is off by default and finds a real class of bug. Add it permanently.
Confirm the sections from outside the program
size layout text data bss dec hex filename
3521 664 400064 404249 62b99 layoutThe bss figure is roughly 400 KB — the 100 000-element zeroed array — yet the executable on disk is only a few kilobytes. Zeroed statics are recorded as a size, not as content. Delete big_zero_array, rebuild, and watch bss collapse.
nm layout | grep -E ' [BDRT] ' | sortThe letter in nm's output is the section: T for text, D for data, B for bss, R for read-only. You can see each variable land exactly where the table in section 3 predicted.
Exhaust the stack on purpose
static void recurse(int depth)
{
char padding[1024];
padding[0] = (char)depth;
printf("\rdepth %d", depth);
recurse(depth + 1);
}ulimit -s # your stack limit in kilobytes
./layout # add a call to recurse(1)It prints an increasing depth and then dies with a segmentation fault — no message, no chance to recover. Divide your stack limit by 1024 and the depth at which it fails should be close. Week 18 does this arithmetic deliberately.
7Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
| Returning a pointer to a local | Dangling pointer once the frame pops | Caller-supplied buffer, static, or heap. |
| Expecting an uninitialized local to be 0 | Garbage; statics are zeroed, locals are not | Initialize every local at declaration. |
| A large array as a local | Stack overflow, no diagnostic | Use the heap — week 19. |
| Shadowing a variable accidentally | The wrong object is modified | Compile with -Wshadow. |
| Globals instead of parameters | Untestable, unreusable, thread-unsafe | Pass state explicitly; group it in a struct. |
| A static local in a function called from threads | Shared state, data race | Make the state a parameter — week 41. |
| Writing through a pointer to a literal | Crash: .rodata is read-only | Declare it const char *. |
| Assuming a static initializer runs at first call | It runs once, before main | Use a flag if you need lazy initialization. |
8Check yourself
Give an example where scope and storage duration differ.
A static local variable. Its scope is the block it is declared in — no other function can name it — but its storage duration is the entire program run, so it retains its value between calls. Scope is about visibility of the name; duration is about lifetime of the object.
Why is a static variable guaranteed to be zero when an uninitialized local is not?
Statics are allocated once at program start, and the loader zeroes the .bss region before main runs. A local is created by adjusting the stack pointer on each call, which does not clear the memory; whatever the previous call left there remains.
Why does a ten-megabyte zeroed global array barely increase the executable's size?
Because it goes in .bss, which records only a size. There is nothing to store — every byte is zero — so the loader simply reserves and clears that much memory at startup. Give the same array a non-zero initializer and it moves to .data, where the contents must actually be in the file.
What happens when the stack runs out, and how would you notice?
The stack grows into an unmapped guard page and the program receives a fatal signal — a segmentation fault with no message and no opportunity to recover. There is no return code to check. The usual causes are unbounded recursion and very large local arrays; both are fixed by moving the data to the heap or bounding the depth.
Name two concrete costs of using a file-scope variable instead of a parameter.
Any two of: you cannot understand a function without tracing every other function that touches the variable; tests must reset shared state and cannot run in parallel; only one instance can ever exist, so the code cannot be reused for two independent sets of data; and concurrent access from threads corrupts it. Passing state explicitly removes all four.
9Where this leads
You can now picture where every object lives. Week 18 uses the stack half of that picture directly: recursion is the construct that makes frames visible, and you will watch them accumulate and unwind in a debugger. Week 19 then uses the heap half, where lifetime stops being automatic and becomes your responsibility.