Memory Management
Dynamic memory allocation with malloc/calloc/realloc/free — heap vs stack, memory leaks, and valgrind.
The four regions of memory and why the heap matters
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:
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...) └──────────────┘
static with an initializer → .data; global or static without an initializer → .bss; returned by malloc → heap; 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 — 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 — 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 */
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
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
| Function | Use when | Initializes? | Include |
|---|---|---|---|
malloc(n) | You'll fill the memory immediately | No (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 allocation | New bytes uninitialized | <stdlib.h> |
free(ptr) | When you're done with any heap allocation | — | <stdlib.h> |
Dynamic allocation in practice
#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;
}
0 1 4 9 16
#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;
}
hello, heap!
#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;
}
Practice problems with solutions
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;
}
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.
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;
}
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.
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);
}
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.
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;
}
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.
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):
- (a)
val— the variable itself - (b)
str— the pointer variable - (c) the string literal
"XYZA"— the data pointed to bystr - (d)
tr - (e)
sp - (f)
x - (g)
holder— the pointer variable - (h) the memory allocated by
malloc(whatholderpoints to) - (i)
*holder— thechar*stored inside the malloc'd block (same value asstr)
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. |
- .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
Test your understanding
malloc return if allocation fails?LO4malloc and calloc?LO4free(p), it is safe to call free(p) again immediately.LO9malloc(n * sizeof(int)) instead of malloc(n * 4)?LO4