Skip to content

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 local appear 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 static locals. Fixed in size and location when the program is built.
  • Heap: memory you request at run time with malloc and give back with free. 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 broken fails: &local points into a frame that is popped the instant broken returns. The address now names storage that belongs to whatever is called next.

Tracing a recursion frame by frame

int fact(int n) {
    if (n <= 1) {
        return 1;
    }
    return n * fact(n - 1);
}

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 n in its own frame - that is why recursion works at all: the three ns (3, 2, 1) coexist without colliding.
  • The frames pop in reverse: fact(1) returns 1, then fact(2) computes 2, then fact(3) computes 6. 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, every char 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 one int needs, so the code is correct on any machine. Never hard-code 4.
  • malloc can fail. It returns NULL when the heap cannot satisfy the request. Always check before you dereference - dereferencing NULL is a crash.
  • Every malloc has exactly one free. Not zero (that leaks), not two (that is a double-free). Pair them in your head as you write the malloc.
  • Setting p = NULL after free is a cheap habit: a later accidental *p then 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;
}
heap int = 42
  • Note that p itself is a normal pointer on the stack; what it points at lives on the heap. When main returns, p disappears - but the block it pointed at would linger forever if we had not called free.
  • Aside (one line, see the practice set): calloc(n, size) allocates and zeroes; realloc(p, newsize) grows or shrinks an existing block. Same family as malloc.

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;
}
How many numbers? 5
0 1 4 9 16
  • malloc(n * sizeof(int)) is the idiom: count times element size. The result is a pointer to the first element, so a[i] works exactly as before - because a[i] is *(a + i), and that is true for any pointer, heap or stack.
  • One malloc, one free. The block stays valid until you free it, no matter how many functions you pass a through 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 an n-element array, fill it from input, compute something over it, and free it. Check malloc'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 a is gone when leaky returns, but the heap block it named is not freed - nothing points to it anymore, so it can never be freed. Call leaky in 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. Setting p = NULL; right after free turns a silent corruption into a loud crash, and makes a second free(NULL) harmless (freeing NULL is 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 ./myprog runs 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_range is popped on return, and a (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 n squares, then have main print it and free it.

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 for NULL; index it like any array; free it exactly once; set the pointer to NULL after.
  • The two heap bugs: leaks (never freed) and dangling pointers (use-after-free, double-free). valgrind finds 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.