Dynamic memory allocation lets a C program request memory at runtime, rather than having all sizes fixed at compile time. The functions are malloc (allocate), free (release), calloc (allocate and zero), and realloc (resize). Memory allocated this way lives on the heap — separate from the stack (function locals, automatic) and global/static segments (program-wide, fixed lifetime).

#include <stdlib.h>
 
int *p = malloc(100 * sizeof(int));    // request space for 100 ints
// ... use p ...
free(p);                                // release when done

malloc(n) reserves n bytes on the heap and returns a pointer to the start. If allocation fails (out of memory), it returns NULL — always check.

Why dynamic

Stack-allocated arrays must have a size known at compile time:

int data[1000];   // fine, size is a constant
int data[n];      // C99 onwards, but only on stack — limited size

If you need to handle a list whose size you don’t know until runtime — say, lines from a file, or nodes in a tree — you allocate dynamically with malloc.

Three classic use cases:

  1. Variable-size collections. Linked list, trees, dynamic arrays — all grow at runtime via malloc.
  2. Long-lived objects. Stack memory disappears when the function returns. To keep an object alive past the return, allocate on the heap.
  3. Saving memory. Allocating only what’s needed beats reserving a large fixed-size buffer.

malloc and free

malloc(size_t bytes) returns a void * (untyped pointer). The C convention since C89 is not to cast it:

int *arr = malloc(n * sizeof(int));

The implicit conversion from void * to int * is automatic in C. Adding (int *) doesn’t help and can actually mask a missing #include <stdlib.h> — without the header, malloc is implicitly declared as returning int, and the cast suppresses what would otherwise be a useful warning. C++ is the exception (it requires the cast), but C++ code uses new instead of malloc anyway. Always multiply by sizeof(type) so the byte count is right for your type.

free(ptr) releases memory that was previously malloc’d. Failing to call free causes a memory leak — the program holds onto memory it no longer uses. Repeated leaks can exhaust available memory.

Calling free on a pointer that wasn’t malloc’d (or that’s already been freed) is undefined behavior — usually a crash.

Allocating a struct

typedef struct {
    char *name;
    int   age;
} Person;
 
Person *bob = (Person *) malloc(sizeof(Person));
bob->name = "Bob";
bob->age  = 42;
// ... use bob ...
free(bob);

The size passed to malloc is sizeof(Person) — the bytes one Person takes up. The returned pointer is then accessed with the arrow operator (->).

Arrays of pointers

A common pattern for collections of structs: instead of allocating an array of structs, allocate an array of pointers to structs, then malloc each struct individually:

Person *employees[100];
 
for (int i = 0; i < 100; i++) {
    employees[i] = malloc(sizeof(Person));
    // ... initialize each ...
}
 
// later:
for (int i = 0; i < 100; i++) {
    free(employees[i]);
}

This uses 800 bytes for the pointer array (on a 64-bit system) plus per-struct allocations. Compared to a flat Person employees[100] (which uses 100 × sizeof(Person)), the pointer-array version is more flexible — you can allocate only the structs you actually need, share structs between arrays, etc.

strdup

For dynamically copying strings:

char *s1 = "Hello";
char *s2 = strdup(s1);   // allocates a new string copy
// ... use s2 ...
free(s2);

strdup is essentially malloc(strlen(s)+1) followed by strcpy. Useful when you want a copy that survives independently of the original.

Function pointers

C also lets you take pointers to functions — variables holding function addresses, which can then be invoked indirectly. They’re a separate primitive from data pointers and not really a memory-allocation topic, so they live in their own note. See Function pointer for syntax, callbacks, dispatch tables, vtables, and the typedef pattern that makes the syntax legible.

Common pitfalls

  • Forgetting to free — memory leak. Long-running programs eventually crash.
  • Use after free — accessing memory after free. Undefined behavior, often crashes.
  • Double freefreeing the same pointer twice. Heap corruption, eventual crash.
  • Forgetting to multiply by sizeofmalloc(100) allocates 100 bytes, not 100 ints. Mismatch between intent and reality.
  • Not checking for NULLmalloc can fail. Without a check, the next line dereferences NULL.

Modern tools (Valgrind, AddressSanitizer) catch most of these at runtime. Modern languages (Java, Go, Rust) handle memory automatically. C makes you do it by hand — the price of speed and control.

For data structures that rely on dynamic allocation, see Linked list, Tree (data structure), Dynamic array.