Memory Management in C
Learn malloc, calloc, realloc, and free, understand heap vs stack, detect memory leaks with Valgrind, and avoid buffer overflows.
Stack vs Heap
C gives you two places to store data: the stack and the heap. Understanding the difference is fundamental to writing correct C programs. The stack is fast and automatic but limited in size; the heap is flexible and large but requires manual management. Choosing the wrong one leads to either stack overflows (too much stack) or memory leaks (forgetting to free heap memory).
#include <stdio.h>
#include <stdlib.h>
void stack_example(void) {
int arr[1000]; /* ~4KB on the stack — fast, zero overhead, dies with the function */
arr[0] = 42;
/* arr is freed automatically when this function returns */
}
int *heap_example(int n) {
int *arr = malloc(n * sizeof(int)); /* on the heap — survives beyond the function */
if (!arr) return NULL;
for (int i = 0; i < n; i++) arr[i] = i;
return arr; /* safe: heap memory outlives the function that allocated it */
}
int main(void) {
stack_example();
int *data = heap_example(10);
if (data) {
printf("data[5] = %d\n", data[5]); /* 5 */
free(data); /* must free heap memory — it does not go away on its own */
data = NULL; /* set to NULL to prevent accidental use-after-free */
}
return 0;
}
Stack: Fast, limited size (typically 1–8 MB), automatically managed. Good for small, short-lived data.
Heap: Slower, virtually unlimited (bounded by available RAM), manually managed. Required for large data, data that outlives a function, or data whose size is not known at compile time.
malloc — Allocate Uninitialized Memory
malloc allocates a block of bytes on the heap and returns a pointer to it. The contents are uninitialized — they contain whatever bytes happened to be in that memory. Always check the return value; malloc returns NULL if allocation fails.
#include <stdlib.h>
#include <stdio.h>
int main(void) {
int n = 100;
/* Allocate n ints — contents are uninitialized (garbage until you write them) */
int *arr = malloc(n * sizeof(int));
if (arr == NULL) {
fprintf(stderr, "malloc failed\n");
return 1;
}
for (int i = 0; i < n; i++) {
arr[i] = i * i;
}
printf("arr[10] = %d\n", arr[10]); /* 100 */
free(arr);
arr = NULL;
/* malloc returns NULL on failure — always check */
size_t huge = (size_t)-1; /* impossibly large */
int *fail = malloc(huge);
if (!fail) {
printf("Allocation failed (expected)\n");
}
return 0;
}
calloc — Allocate Zero-Initialized Memory
calloc allocates memory and zeroes it out before returning. This is slightly slower than malloc but eliminates uninitialized-read bugs for data structures that rely on zero as a default value — hash tables, sparse arrays, and counters.
#include <stdlib.h>
#include <stdio.h>
int main(void) {
int rows = 4, cols = 4;
/* calloc(count, size) — allocates count*size bytes, all set to zero */
int *matrix = calloc(rows * cols, sizeof(int));
if (!matrix) return 1;
/* All elements start at 0 — no need to initialize explicitly */
matrix[1 * cols + 2] = 42; /* matrix[1][2] = 42 */
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
printf("%3d", matrix[r * cols + c]);
}
printf("\n");
}
free(matrix);
return 0;
}
Use calloc when you need zeroed memory. It is often slightly slower than malloc but avoids uninitialized-read bugs.
realloc — Resize an Allocation
realloc changes the size of an existing allocation. It may move the data to a new location if the current block cannot be expanded in place. The classic use case is a dynamic array (like C++‘s std::vector) that doubles its capacity whenever it fills up.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/* Dynamic array that grows automatically as elements are added */
typedef struct {
int *data;
size_t size;
size_t capacity;
} IntVec;
int vec_push(IntVec *v, int value) {
if (v->size == v->capacity) {
size_t new_cap = v->capacity == 0 ? 4 : v->capacity * 2;
/* CRITICAL: use a temp pointer — if realloc fails, the original is still valid */
int *tmp = realloc(v->data, new_cap * sizeof(int));
if (!tmp) return -1; /* v->data is still valid — caller can recover */
v->data = tmp;
v->capacity = new_cap;
}
v->data[v->size++] = value;
return 0;
}
int main(void) {
IntVec v = {NULL, 0, 0};
for (int i = 0; i < 20; i++) {
if (vec_push(&v, i * 10) != 0) {
fprintf(stderr, "Push failed\n");
break;
}
}
for (size_t i = 0; i < v.size; i++) {
printf("%d ", v.data[i]);
}
printf("\n");
printf("size=%zu, capacity=%zu\n", v.size, v.capacity);
free(v.data);
return 0;
}
Never do ptr = realloc(ptr, new_size) — if realloc returns NULL, you’ve lost the original pointer and leaked the memory. Always use a temporary variable.
free — Release Memory
free returns memory to the heap. Every malloc/calloc/realloc must eventually be paired with a free. Forgetting to free causes memory leaks; freeing the same pointer twice (double-free) corrupts the allocator’s internal state and can be exploited as a security vulnerability.
#include <stdlib.h>
#include <string.h>
typedef struct Node {
int value;
struct Node *next;
} Node;
/* Free every node in a linked list — save next BEFORE freeing the current node */
void free_list(Node *head) {
while (head) {
Node *next = head->next; /* save before free — after free, head is invalid */
free(head);
head = next;
}
}
int main(void) {
/* Build a small list */
Node *head = NULL;
for (int i = 5; i >= 1; i--) {
Node *n = malloc(sizeof(Node));
n->value = i;
n->next = head;
head = n;
}
free_list(head);
head = NULL;
return 0;
}
Rules for free:
- Only free memory obtained from
malloc,calloc, orrealloc - Free each allocation exactly once — double-free is undefined behavior
free(NULL)is safe and does nothing- Set pointers to
NULLafter freeing to detect use-after-free bugs
Memory Leaks and Valgrind
A memory leak occurs when allocated memory is never freed. In a long-running process, leaks grow the process’s memory consumption until the OS kills it. Valgrind and AddressSanitizer make leaks visible immediately, pointing to the exact line where the leaked memory was allocated.
/* Intentional leak for demonstration */
void leaky_function(void) {
int *buf = malloc(1024);
/* forgot to free(buf) before returning — 1024 bytes leaked */
}
Compile with debug symbols and run under Valgrind:
gcc -Wall -std=c11 -g -o prog program.c
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./prog
Valgrind output for a leak:
==12345== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2FB0F: malloc (vg_replace_malloc.c:309)
==12345== by 0x10868B: leaky_function (program.c:4)
==12345== by 0x1086A1: main (program.c:9)
On Linux/macOS with GCC or Clang, AddressSanitizer catches leaks and corruptions at runtime:
gcc -Wall -std=c11 -g -fsanitize=address,undefined -o prog program.c
./prog
Buffer Overflow Example
A buffer overflow writes past the end of an allocated buffer, corrupting adjacent memory. This is the root cause of countless security vulnerabilities. The fix is always the same: use length-limited functions and verify that your buffers are large enough.
#include <string.h>
#include <stdio.h>
/* DANGEROUS — do not write code like this */
void vulnerable(const char *input) {
char buf[8];
strcpy(buf, input); /* no bounds check — overflows if input > 7 chars */
printf("buf: %s\n", buf);
}
/* SAFE version — snprintf always null-terminates and never overflows */
void safe_version(const char *input) {
char buf[8];
snprintf(buf, sizeof(buf), "%s", input); /* truncates gracefully */
printf("buf: %s\n", buf);
}
int main(void) {
safe_version("hello"); /* OK */
safe_version("hello world"); /* truncated to "hello w" but safe */
return 0;
}
Memory Layout Summary
Understanding where different variables live helps you reason about lifetime, size limits, and what can go wrong:
High address
┌──────────────────┐
│ Stack │ ← grows downward; local variables, function frames
│ ↓ │
├──────────────────┤
│ │
│ (free space) │
│ │
├──────────────────┤
│ ↑ │
│ Heap │ ← grows upward; malloc/calloc/realloc
├──────────────────┤
│ BSS segment │ ← uninitialized global/static variables (zero-filled)
├──────────────────┤
│ Data segment │ ← initialized global/static variables
├──────────────────┤
│ Text segment │ ← program code (read-only)
└──────────────────┘
Low address