C in 100 Seconds: Stack vs Heap — Where Variables Live
Video: C in 100 Seconds: Stack vs Heap — Where Variables Live | Episode 31 by Taught by Celeste AI - AI Coding Coach
C Stack vs Heap: Where Variables Live
Stack: fast, automatic, scope-limited. Heap: slow, manual, lives until you
free. Returning a pointer to a stack local is a dangling-pointer disaster.
C exposes the memory hierarchy directly. Knowing where each variable lives — stack, heap, or static — is the difference between a working program and a use-after-free crash.
The basic shape
#include <stdio.h>
#include <stdlib.h>
int *stack_danger() {
int x = 42;
return &x; // dangling pointer!
}
int *heap_safe() {
int *p = malloc(sizeof(int));
*p = 42;
return p; // caller owns it
}
int main() {
int a = 10; // stack
int arr[5] = {1, 2, 3, 4, 5}; // stack
int *h = malloc(sizeof(int)); // heap
*h = 99;
printf("Stack: %d, Heap: %d\n", a, *h);
free(h);
int *safe = heap_safe();
printf("Heap return: %d\n", *safe);
free(safe);
// int *bad = stack_danger();
// printf("%d\n", *bad); // undefined!
return 0;
}
The four memory regions
+------------------+ high address
| stack | ↓ grows down
|------------------|
| ↓ |
| (free) |
| ↑ |
|------------------|
| heap | ↑ grows up
|------------------|
| bss/data | globals & static
|------------------|
| text | code
+------------------+ low address
- Text — your compiled code. Read-only. Includes string literals.
- Data/BSS — globals and
staticvariables.datafor initialized;bssfor zero-initialized. - Heap — dynamically allocated (
malloc). Grows toward higher addresses. - Stack — function-call frames, locals. Grows toward lower addresses (on most architectures).
Stack: fast and automatic
void f() {
int x = 5; // stack
int arr[100]; // stack
} // x and arr are gone
Each function call allocates a stack frame with space for its locals. When the function returns, the frame is popped — locals are gone.
Pros:
- Fast. Allocation is just adjusting the stack pointer (one instruction).
- Automatic. No
freeneeded. - Cache-friendly. Stack frames are usually hot in cache.
Cons:
- Limited size. Default stack is typically 1-8 MB. Big arrays blow it.
- Scope-limited. Locals are gone when the function returns.
- Size known in advance. (Mostly — VLAs let you allocate runtime-sized stack arrays.)
Heap: manual and flexible
void f() {
int *p = malloc(sizeof(int)); // heap
*p = 5;
// ...
free(p);
}
Pros:
- Unlimited size (up to RAM).
- Outlives the function. Returns can hand back heap pointers.
- Runtime size. Allocate as much as you decide at runtime.
Cons:
- Slow.
mallochas to find a free block, update bookkeeping. - Manual. Forget
free→ leak. Free twice → crash. - Fragmentation. Long-running programs can fragment.
The dangling pointer trap
int *bad() {
int x = 42;
return &x; // x is on the stack — destroyed when bad() returns
}
int *p = bad();
*p; // UNDEFINED — x is gone; this address now belongs to nobody
When bad() returns, its stack frame is popped. The address &x is invalid. Reading or writing through p is undefined behaviour — sometimes works (data hasn't been overwritten yet), sometimes catastrophic.
The fix: heap allocation.
int *good() {
int *p = malloc(sizeof(int));
*p = 42;
return p;
}
int *p = good();
*p; // OK — heap memory persists
free(p);
Static and global variables
int global = 5; // data segment — lives entire program
static int s = 10; // also data; file-scoped or function-scoped
void f() {
static int counter = 0; // initialized once; persists across calls
counter++;
}
static inside a function: the variable persists across calls. Initialized once, retains value.
Globals and statics are zero-initialized by default. Local stack variables are not.
String literals
char *s = "hello"; // pointer to read-only memory
char arr[] = "hello"; // copy in stack-or-data segment
"hello" lives in the text segment (read-only). char *s = "hello" — s points there. char arr[] = "hello" copies the literal into a writable array (typically on the stack).
s[0] = 'H'; undefined. arr[0] = 'H'; OK.
sizeof on stack vs heap arrays
int stack[10];
int *heap = malloc(10 * sizeof(int));
sizeof(stack) // 40 (knows the array size)
sizeof(heap) // 8 (just a pointer)
sizeof is a compile-time operator. For stack-allocated arrays, the compiler knows the size. For heap arrays, all you have is a pointer.
This is why you must track heap array sizes manually:
typedef struct {
int *data;
size_t size;
} IntList;
Variable-length arrays (VLA)
void f(int n) {
int arr[n]; // VLA — size determined at call time
for (int i = 0; i < n; i++) arr[i] = i;
}
C99 added VLAs — stack arrays whose size isn't a compile-time constant. They use stack space, so beware large n (stack overflow). C11 made them optional; some platforms don't support.
For runtime-sized memory, prefer malloc.
The stack overflow
void recurse() {
char buf[1000000]; // 1 MB per call
recurse();
}
Each call adds a 1MB frame. Default stack ~8MB → ~8 levels deep before crash.
Same for deep recursion without large locals — eventually the stack fills.
Symptoms: segfault on what looks like an innocent function call.
Fixes:
- Smaller per-call locals.
- Iterative version.
- Heap-allocate the buffer.
- Increase stack size (
ulimit -s).
Common mistakes
Returning pointer to local. Dangling pointer — undefined behaviour.
Passing pointer to local across thread boundaries. If the local goes out of scope before the other thread reads, dangling.
Allocating huge arrays on the stack. int arr[10000000]; overflows on most systems. Use heap.
Forgetting to free heap memory. Memory leak (episode 22).
Assuming static means thread-safe. It doesn't — multiple threads share static variables. Use locks or thread-local storage.
Mixing stack/heap in container ownership. A struct with a int *data could be either; the struct's destructor needs to know to call free only for heap.
What's next
Episode 32: string functions — strcat, strchr, strstr. More from the <string.h> toolkit.
Recap
Stack — fast, automatic cleanup, scope-limited, size limit (~8 MB), returning pointer to local = disaster. Heap — slow, manual free, unlimited, persists until freed. Static/global — lives entire program; zero-initialized. Text — code + string literals; read-only. Use stack for short-lived locals; heap for big or long-lived data. Returning data from a function: return by value (cheap), pointer to heap (caller frees), or fill caller's buffer (caller manages).
Next episode: more string functions.