The four regions of memory and why the heap matters

Hotel Analogy

malloc is like calling hotel reception to book a room — you specify how many bytes you need and get back a room number (pointer). free is checking out — you tell reception "I'm done, give that room to someone else." A memory leak is never checking out — you leave the room forever, the hotel fills up, and eventually no rooms are available. A double-free is checking out the same room twice — reception gets confused and the whole hotel may crash.

In Python and Java, memory management is automatic — a garbage collector finds objects that are no longer referenced and reclaims their memory. In C, you are the garbage collector. Every byte you allocate with malloc must eventually be returned with free. The compiler will not warn you if you forget.

A C process has four main memory regions:

Text (Code)
Compiled machine code of your program. Read-only. Fixed size.
Data (Globals & Statics)
Global variables, static variables. Initialized at program start. Fixed size.
Heap (Dynamic) ↑ grows up
Where malloc/calloc/realloc allocate. You control this. Flexible size. Manual management required.
Stack (Automatic) ↓ grows down
Local variables, function call frames. Automatic: allocated on function entry, freed on return. Fast but limited (~8MB).
Linux process address space (x86-64)
High addresses (0x7fff...)  ┌──────────────┐
                                Stack      ← local vars, grows downward
                            │      ↓       │
                            │              │
                            │      ↑       │
                            │    Heap      │ ← malloc, grows upward
                            ├──────────────┤
                            │  .bss        │ ← uninit globals/statics (zeroed)
                            │  .data       │ ← init globals/statics
                            │  .text       │ ← code + string literals (read-only)
Low addresses  (0x0000...)  └──────────────┘
Rule of thumb: if a variable is local to a function → stack; declared global or static with an initializer → .data; global or static without an initializer → .bss; returned by mallocheap; a string literal in quotes → .text.

Stack vs Heap in practice: When you declare int arr[100]; inside a function, those 400 bytes live on the stack — fast, automatic, gone when the function returns. When you write int *arr = malloc(100 * sizeof(int));, those bytes live on the heap — they survive until you explicitly free them. Use the heap when: (1) you don't know the size at compile time, (2) the data must outlive the current function, or (3) the data is too large for the stack.

Python / Java (automatic)
# Python — garbage collected
data = [0] * 100    # list on heap
data = None         # now eligible for GC
# GC reclaims memory automatically
# No malloc, no free, no leaks
C (manual)
/* C — you manage the heap */
int *data = malloc(100 * sizeof(int));
if (data == NULL) { /* handle error */ }

/* use data ... */

free(data);     /* YOU must do this */
data = NULL;    /* good practice */
Three classes of undefined behavior to avoid

Memory leak: malloc without a matching free — program RAM usage grows until the OS kills it.
Double-free: calling free() twice on the same pointer — corrupts the allocator's internal data structures, causes crashes or security vulnerabilities.
Use-after-free: accessing a pointer after free() — the memory may be reallocated for something else; you read/write garbage, or corrupt another object.

malloc, calloc, realloc, free — annotated

/* malloc — allocate n bytes, uninitialized */
void *malloc(size_t n);
//  ^  returns void* (generic pointer) or NULL on failure
//  n = number of BYTES requested

int *arr = malloc(10 * sizeof(int));
//  ^          ^    always use sizeof — never hardcode 4
if (arr == NULL) { /* ALWAYS check */ }

/* calloc — allocate n elements of size bytes, ZERO-INITIALIZED */
void *calloc(size_t n, size_t size);
int *arr = calloc(10, sizeof(int));
// all 10 ints start as 0 — unlike malloc which leaves garbage

/* realloc — resize an existing allocation */
void *realloc(void *ptr, size_t new_size);
// may move the data to a new location — use returned pointer, not old ptr
// if new_size == 0 acts like free; if ptr == NULL acts like malloc
int *tmp = realloc(arr, 20 * sizeof(int));
if (tmp == NULL) { /* realloc failed — arr still valid */ }
else { arr = tmp; }  // update arr to new location

/* free — release the allocation */
void free(void *ptr);
free(arr);
arr = NULL;  // set to NULL — prevents accidental use-after-free
Key rule: Always use sizeof in malloc — never write malloc(n * 4). The size of int is platform-dependent. malloc(n * sizeof(int)) is always correct.

malloc vs calloc vs realloc — when to use each

FunctionUse whenInitializes?Include
malloc(n)You'll fill the memory immediatelyNo (garbage)<stdlib.h>
calloc(n, size)You want zeroed memory (e.g. arrays)Yes (all zeros)<stdlib.h>
realloc(ptr, new_size)Growing/shrinking an existing allocationNew bytes uninitialized<stdlib.h>
free(ptr)When you're done with any heap allocation<stdlib.h>

Dynamic allocation in practice

Example 1 — Dynamic array with malloc Week 4 Lecture
#include <stdio.h>
#include <stdlib.h>   /* malloc, free, calloc, realloc */

int main(void) {
    int n;
    printf("How many integers? ");
    scanf("%d", &n);

    /* Allocate n integers on the heap */
    int *arr = malloc(n * sizeof(int));
    if (arr == NULL) {
        fprintf(stderr, "malloc failed\n");
        return 1;
    }

    /* Fill with values */
    for (int i = 0; i < n; i++) {
        arr[i] = i * i;   /* arr[i] works just like a normal array */
    }

    /* Print them */
    for (int i = 0; i < n; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    /* MUST free when done */
    free(arr);
    arr = NULL;   /* good practice: prevents dangling pointer use */
    return 0;
}
Sample run (n=5)
How many integers? 5
0 1 4 9 16
Example 2 — Dynamic string duplication (like strdup) Week 4 Lecture
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Allocate a copy of a string on the heap */
char *my_strdup(const char *s) {
    size_t len = strlen(s) + 1;   /* +1 for null terminator */
    char *copy = malloc(len);
    if (copy == NULL) return NULL;
    memcpy(copy, s, len);          /* copy all bytes including '\0' */
    return copy;
}

int main(void) {
    char *s = my_strdup("Hello, heap!");
    if (s == NULL) { fprintf(stderr, "malloc failed\n"); return 1; }

    printf("%s\n", s);
    s[0] = 'h';   /* can modify — it's our own copy */
    printf("%s\n", s);

    free(s);   /* every malloc must have a matching free */
    return 0;
}
Output
Hello, heap!
hello, heap!
Example 3 — Growing array with realloc Tutorial Wk6A
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int capacity = 4;
    int size = 0;
    int *arr = malloc(capacity * sizeof(int));
    if (!arr) return 1;

    /* Read integers until EOF, grow array as needed */
    int val;
    while (scanf("%d", &val) == 1) {
        if (size == capacity) {
            capacity *= 2;   /* double capacity */
            int *tmp = realloc(arr, capacity * sizeof(int));
            if (tmp == NULL) {
                free(arr);   /* realloc failed — arr still valid, free it */
                fprintf(stderr, "realloc failed\n");
                return 1;
            }
            arr = tmp;   /* update pointer — old pointer may be invalid now */
        }
        arr[size++] = val;
    }

    printf("Read %d values\n", size);
    free(arr);
    return 0;
}
Sample run (input: 1 2 3 4 5 6 7 8)
Read 8 values

Practice problems with solutions

P1 — Identify the memory leak Exam 2024

How many bytes are leaked? Where is the leak? Fix it.

#include <stdlib.h>
#include <string.h>

char *get_greeting(const char *name) {
    char *buf = malloc(100);
    strcpy(buf, "Hello, ");
    strcat(buf, name);
    return buf;
}

int main(void) {
    char *g = get_greeting("Alice");
    printf("%s\n", g);
    /* nothing else */
    return 0;
}
int main(void) {
    char *g = get_greeting("Alice");
    printf("%s\n", g);
    free(g);   /* FIX: free the returned heap pointer */
    return 0;
}
The leak: get_greeting calls malloc(100) and returns the pointer. The caller receives it in g and is responsible for freeing it. Without free(g), 100 bytes are leaked. The rule is: whoever owns the pointer is responsible for freeing it — and the function's contract (via comments or documentation) should make this clear.
P2 — Fix the double-free Exam 2024

What is wrong with this code? What will happen at runtime? Fix it.

#include <stdlib.h>
int main(void) {
    int *p = malloc(sizeof(int));
    *p = 42;
    free(p);
    free(p);   /* ??? */
    return 0;
}
#include <stdlib.h>
int main(void) {
    int *p = malloc(sizeof(int));
    *p = 42;
    free(p);
    p = NULL;   /* FIX: set to NULL after free */
    /* free(NULL) is safe — defined to do nothing */
    free(p);    /* now harmless */
    return 0;
}
Double-free is undefined behavior. The first free(p) releases the memory and marks it available in the allocator's free list. The second free(p) corrupts the allocator's internal metadata — it may crash with "double free or corruption", silently corrupt other heap data, or be exploited as a security vulnerability. The fix: set the pointer to NULL immediately after freeing. free(NULL) is defined to do nothing.
P3 — Draw the memory layout Week 4 Lecture

For the code below, identify which variables are on the stack, which are on the heap, and what value each pointer holds after the malloc call.

int global_x = 10;      /* where does this live? */

void foo(void) {
    int local_y = 20;   /* where? */
    int *ptr = malloc(sizeof(int));  /* where is ptr? where is *ptr? */
    *ptr = 30;
    free(ptr);
}
global_x — in the data segment (global/static storage). Lives for the entire program lifetime.

local_y — on the stack. Created when foo() is called, destroyed when foo() returns.

ptr — the pointer variable itself is on the stack (it's a local variable). Its value is a heap address returned by malloc.

*ptr (the int) — on the heap. The 4 bytes malloc returned are at some heap address, which ptr holds.

After free(ptr): the heap memory is released, but ptr still holds the old address (dangling pointer). Accessing *ptr after free is undefined behavior. Set ptr = NULL to prevent this.
P4 — Predict the output and spot the bug Exam 2024 style

What does this program print? What is the bug?

#include <stdio.h>
#include <stdlib.h>

int *make_array(int n) {
    int arr[n];   /* VLA on stack */
    for (int i = 0; i < n; i++) arr[i] = i;
    return arr;   /* returning address of stack variable! */
}

int main(void) {
    int *p = make_array(5);
    printf("%d\n", p[0]);   /* undefined behavior */
    return 0;
}
#include <stdio.h>
#include <stdlib.h>

int *make_array(int n) {
    int *arr = malloc(n * sizeof(int));  /* FIX: allocate on heap */
    if (!arr) return NULL;
    for (int i = 0; i < n; i++) arr[i] = i;
    return arr;   /* heap memory survives the function return */
}

int main(void) {
    int *p = make_array(5);
    if (!p) return 1;
    printf("%d\n", p[0]);   /* prints 0 -- well-defined */
    free(p);                 /* caller must free */
    return 0;
}
The bug: arr is a Variable Length Array (VLA) on the stack of make_array. When the function returns, that stack frame is gone — the memory at arr's address may be overwritten by subsequent function calls. Returning a pointer to a local variable is undefined behavior. The fix is to use malloc so the data survives the function return; the caller is then responsible for free.
P5 — Identify the memory segment for each variable From: 2024 Final Exam Q5 — 15 marks

Given the following program, identify which memory segment each variable listed below lives in at the point marked ★ (line 10, just before return 0).

#include <stdlib.h>
#include <string.h>

static char val = -47;
char *str = "XYZA";

int main(void) {
    char tr = str[2];
    char *sp = &val;
    char x = *sp + 2;
    char **holder = malloc(sizeof(char *));
    *holder = str;
    /* ★ point of analysis */
    return 0;
}

For each of the following, name the memory segment (.text / .data / .bss / heap / stack):

  1. (a) val — the variable itself
  2. (b) str — the pointer variable
  3. (c) the string literal "XYZA" — the data pointed to by str
  4. (d) tr
  5. (e) sp
  6. (f) x
  7. (g) holder — the pointer variable
  8. (h) the memory allocated by malloc (what holder points to)
  9. (i) *holder — the char* stored inside the malloc'd block (same value as str)
Key Insight

Pointers themselves live somewhere (stack, data segment, etc.) but the data they point to lives somewhere else entirely. Always distinguish "where the pointer variable is stored" from "where it points to".

Variable / Expression Segment Why
val (the variable) .data static char val = -47 — initialized static variable. Static storage, lives for the entire program lifetime.
str (the pointer) .data Global pointer initialized to a non-zero value ("XYZA"'s address). Initialized globals go in .data.
"XYZA" (the literal) .text String literals are read-only data embedded in the text (code) segment. Attempting to modify them is undefined behavior.
tr stack Local variable declared inside main(). Automatically allocated on the stack when main is entered.
sp stack Local pointer variable in main(). The pointer itself lives on the stack; it stores the address of val (which is in .data).
x stack Local variable in main(). Value is computed from *sp + 2 but the variable itself is on the stack.
holder (the pointer) stack Local char ** variable in main(). The pointer variable itself is on the stack; it stores a heap address.
malloc'd block (*holder storage) heap Allocated with malloc(sizeof(char *)). All malloc/calloc/realloc allocations live on the heap until freed.
*holder (value stored) .text (address) *holder = str copies str's value (the address of "XYZA") into the heap block. The value is an address pointing into the text segment — it is itself stored in the heap block.
Summary of segments involved:
  • .text segment — machine code + read-only string literals like "XYZA"
  • .data segment — initialized globals and statics: val, str
  • .bss segment — uninitialized globals and statics (none here, but e.g. static int count; would go here)
  • heap — the block returned by malloc
  • stack — all locals in main: tr, sp, x, holder

Note: val is declared with the static keyword at file scope — even without static it would be a global (in .data or .bss). The static keyword at file scope only limits its linkage (not visible outside this translation unit), not where it lives in memory.

Key concepts to memorize

Card 1 of 10
Question — click to flip
Answer
Click card to flip • Use buttons to navigate

Test your understanding

Topic 12 Quiz — Memory Management Score: 0 / 6
1
What does malloc return if allocation fails?LO4
multiple choice
2
What is a memory leak?LO9
multiple choice
3
What is the difference between malloc and calloc?LO4
multiple choice
4
True or False: After calling free(p), it is safe to call free(p) again immediately.LO9
true / false
5
Why should you write malloc(n * sizeof(int)) instead of malloc(n * 4)?LO4
multiple choice
6
Fill in: The command-line tool used to detect memory leaks and invalid heap accesses in C programs is ___.LO9
fill in the blank
0/6
Quiz complete!