Lecture 6 - The Call Stack, Stack vs Heap, and Heap Allocation¶
This lecture answers the question Lecture 5 left open: where do local variables
actually live, such that they appear on each call and vanish on return? The answer
is the call stack. From there we contrast the stack with the heap - memory
whose lifetime you control - and learn to allocate and free it with malloc and
free.
1. Recap and scope/lifetime catch-up¶
- Last time: a pointer is a value that is an address;
&makes one,*follows it. Passing a pointer lets a function change the caller's variable (swap, out-parameters). - We ended on a bug: a function that returns the address of a local hands back a dangling pointer, because the local is destroyed when the function returns.
int *broken(void) {
int local = 42;
return &local; /* local dies as we return; the address is now dead */
}
- The open question: why does
localappear on each call and vanish on return? And how do we get memory that outlives the call? Today's two answers: the call stack (automatic, per-call) and the heap (manual, yours to keep).
2. How a program is laid out in memory¶
Before we open up the stack, let us see the whole picture: when you run a program, the operating system loads it into memory and hands it a block of address space organized into a few fixed regions. Everything the program does happens inside these regions.
high addresses
+---------------------+
| Stack | local variables, one frame per call; grows DOWN
| | |
| v |
| |
| ^ |
| | |
| Heap | malloc / free; grows UP
+---------------------+
| Globals and statics | global and static variables (data)
+---------------------+
| Code (text) | the machine instructions of your program
+---------------------+
low addresses
- Code (text): the compiled machine instructions - the functions you wrote. This region is read-only; the program runs it but does not change it.
- Globals and statics: variables that live for the whole run of the program,
such as globals and
staticlocals. Fixed in size and location when the program is built. - Heap: memory you request at run time with
mallocand give back withfree. It grows upward toward higher addresses as you allocate more. Sections 3 onward are all about this region. - Stack: one frame per function call, holding that call's locals and parameters. It grows downward as calls nest and shrinks as they return. This is where we go next.
The heap and stack grow toward each other from opposite ends of the free space in between, which is how a program can spend that space on whichever one it needs.
3. The function call stack¶
Local variables live in a region of memory called the stack. Each time you call a function, the program sets aside a stack frame for that call.
What a stack frame holds¶
- The call's local variables and parameters.
- The return address: where to resume in the caller once this function returns.
- Bookkeeping to restore the caller's frame.
Push on call, pop on return¶
- Calling a function pushes a new frame on top of the stack. Returning pops it off. The stack grows and shrinks like a stack of plates - last on, first off.
- This is exactly the lifetime rule from Lecture 5, seen from the memory side: a local exists for as long as its frame is on the stack. When the function returns, the frame is popped and that storage is handed back for the next call to reuse.
- That is why
brokenfails:&localpoints into a frame that is popped the instantbrokenreturns. The address now names storage that belongs to whatever is called next.
Tracing a recursion frame by frame¶
Calling fact(3) stacks up three frames before any returns:
top -> fact(n = 1) returns 1
fact(n = 2) waiting on fact(1), then returns 2 * 1 = 2
bottom fact(n = 3) waiting on fact(2), then returns 3 * 2 = 6
- Each call gets its own
nin its own frame - that is why recursion works at all: the threens (3,2,1) coexist without colliding. - The frames pop in reverse:
fact(1)returns1, thenfact(2)computes2, thenfact(3)computes6. It runs both directions: pushing on the way down, popping on the way up. - Aside: runaway recursion with no base case keeps pushing frames until the stack runs out of room - a stack overflow.
Exercise - trace the frames¶
Exercises handout, Part A (pen and paper).
- Trace the frames of a small recursion by hand and mark when each local is created and destroyed.
4. Stack vs heap¶
A running C program has two regions where your data can live:
| Stack | Heap | |
|---|---|---|
| Lifetime | Automatic: tied to the function call | Manual: until you free it |
| Created by | Entering a function / block | malloc and friends |
| Destroyed by | Returning from the function | free |
| Size | Fixed when you write the code | Decided at run time |
| Managed by | The compiler, for you | You |
- Everything so far - every local
int, everychar word[256]- has lived on the stack. Its size was a compile-time constant and its lifetime was the function call. - The heap answers the two things the stack cannot: memory whose size you only
learn at run time (the user says how many), and memory that must outlive
the function that created it (the fix for
broken). - The cost: heap memory is yours to manage. Nothing frees it automatically. You ask for it, you give it back. Forgetting either side is the source of the bugs in segment 7.
5. Heap allocation - malloc and free¶
malloc ("memory allocate") asks the heap for a block of bytes and returns a
pointer to it. free gives that block back. Both live in <stdlib.h>.
The shape of an allocation¶
#include <stdlib.h>
int *p = malloc(sizeof(int)); /* room for one int on the heap */
if (p == NULL) { /* malloc returns NULL if it could not allocate */
/* out of memory: handle it, do not dereference p */
return 1;
}
*p = 99; /* use it like any int through the pointer */
printf("%d\n", *p);
free(p); /* give the block back when done */
p = NULL; /* avoid using a freed pointer by accident */
sizeof(int)asks the compiler how many bytes oneintneeds, so the code is correct on any machine. Never hard-code4.malloccan fail. It returnsNULLwhen the heap cannot satisfy the request. Always check before you dereference - dereferencingNULLis a crash.- Every
mallochas exactly onefree. Not zero (that leaks), not two (that is a double-free). Pair them in your head as you write themalloc. - Setting
p = NULLafterfreeis a cheap habit: a later accidental*pthen crashes loudly instead of corrupting whatever now owns that block.
Example - one int on the heap¶
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof(int));
if (p == NULL) {
return 1;
}
*p = 41;
*p = *p + 1;
printf("heap int = %d\n", *p);
free(p);
p = NULL;
return 0;
}
- Note that
pitself is a normal pointer on the stack; what it points at lives on the heap. Whenmainreturns,pdisappears - but the block it pointed at would linger forever if we had not calledfree. - Aside (one line, see the practice set):
calloc(n, size)allocates and zeroes;realloc(p, newsize)grows or shrinks an existing block. Same family asmalloc.
6. Dynamic arrays¶
The real payoff: an array whose size is decided at run time. Lecture 4's
int a[5] had to be a compile-time constant. With malloc you allocate exactly as
many elements as you need, when you know the count.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n;
printf("How many numbers? ");
scanf("%d", &n);
int *a = malloc(n * sizeof(int)); /* room for n ints, chosen at run time */
if (a == NULL) {
return 1;
}
for (int i = 0; i < n; i++) {
a[i] = i * i; /* index a heap block exactly like a real array */
}
for (int i = 0; i < n; i++) {
printf("%d ", a[i]);
}
printf("\n");
free(a); /* one free for the one malloc */
return 0;
}
malloc(n * sizeof(int))is the idiom: count times element size. The result is a pointer to the first element, soa[i]works exactly as before - becausea[i]is*(a + i), and that is true for any pointer, heap or stack.- One
malloc, onefree. The block stays valid until you free it, no matter how many functions you passathrough in between. - This is the array Lecture 4 could not give you: its size is
n, a value typed in at run time, not a constant baked into the program.
Exercise - a dynamic array¶
Exercises handout, Part B, Exercise B1 (on the computer).
- Read
n, allocate ann-element array, fill it from input, compute something over it, andfreeit. Checkmalloc's result before using it.
7. Leaks and dangling pointers¶
Manual memory means two new bug classes. Both compile cleanly and may even appear to work; that is what makes them dangerous.
Memory leak - forgetting free¶
void leaky(void) {
int *a = malloc(100 * sizeof(int));
/* ... use a ... */
return; /* BUG: never freed; the block is now unreachable but still held */
}
- The pointer
ais gone whenleakyreturns, but the heap block it named is not freed - nothing points to it anymore, so it can never be freed. Callleakyin a loop and the program's memory use climbs until it dies. A leak is unreleased memory.
Dangling pointer - using memory after free¶
int *p = malloc(sizeof(int));
*p = 5;
free(p); /* block returned to the heap */
*p = 7; /* BUG: use-after-free - p now dangles */
free(p); /* BUG: double-free of an already-freed block */
- After
free(p), the block is no longer yours. Reading or writing*p(use-after-free) and freeing it again (double-free) are both undefined behavior. Settingp = NULL;right afterfreeturns a silent corruption into a loud crash, and makes a secondfree(NULL)harmless (freeingNULLis defined to do nothing). - The mirror image of Lecture 5's dangling-into-the-stack bug: there the storage died because a frame popped; here it dies because you freed it. Same lesson - a pointer is only as good as the lifetime of what it points at.
- Tool note:
valgrind ./myprogruns your program and reports leaks, use-after-free, and double-frees. It is how you catch these in practice.
8. Returning heap memory from a function¶
Now we can finally fix broken. A function cannot safely return the address of
a local (it dies with the frame), but it can return a pointer to heap memory,
because the heap block outlives the call.
#include <stdlib.h>
int *make_range(int n) {
int *a = malloc(n * sizeof(int));
if (a == NULL) {
return NULL; /* let the caller see the failure */
}
for (int i = 0; i < n; i++) {
a[i] = i;
}
return a; /* safe: the heap block lives on after we return */
}
int main(void) {
int *r = make_range(5);
if (r == NULL) {
return 1;
}
for (int i = 0; i < 5; i++) {
printf("%d ", r[i]); /* 0 1 2 3 4 */
}
printf("\n");
free(r); /* the CALLER now owns it and must free it */
return 0;
}
- The frame of
make_rangeis popped on return, anda(the local pointer) disappears with it - but the heap block it pointed at is untouched by that pop. The returned address is still valid. - Crucial rule: ownership transfers to the caller. Whoever receives the pointer
is now responsible for the one
free. Document who frees what; most leaks are a confusion over who owned the block. - This is the clean answer to Lecture 5's
broken: do not return&local, return heap memory.
Exercise - return a heap array¶
Exercises handout, Part B, Exercise B2 (on the computer).
- Write a function that builds and returns a heap array of the first
nsquares, then havemainprint it andfreeit.
9. Wrap-up¶
- Local variables live in stack frames: pushed on call, popped on return. That is the lifetime rule from Lecture 5 seen in memory, and why returning a local's address dangles.
- The stack is automatic and fixed-size; the heap is manual and sized at run time. Use the heap when the size is not known until run time or the memory must outlive its function.
malloc(count * sizeof(T))allocates; check forNULL; index it like any array;freeit exactly once; set the pointer toNULLafter.- The two heap bugs: leaks (never freed) and dangling pointers
(use-after-free, double-free).
valgrindfinds them. - A function may return heap memory but never the address of a local; when
it does, ownership (the duty to
free) passes to the caller. - Cliffhanger: next week we put heap-owned memory to work - strings you build
and grow, and
structs that bundle related data, the start of real data structures.