Dynamic Memory Management in C: malloc, calloc, realloc and Pitfalls

Motivation for Runtime Allocation

Automatic variables and fixed-size arrays live on the stack and their sizes are frozen at compile time. When the required amount of data is not known until the program is running, the heap offers a flexible alternative.

Core Allocation Routines

All prototypes live in <stdlib.h>.

malloc

void *malloc(size_t bytes);

Requests a contiguous block of at least bytes bytes. On success the pointer is suitably aligned for any object type; on failure it returns NULL. The contents are indeterminate.

Example – allocate ten ints, fill and print:

int *values = malloc(10 * sizeof *values);
if (!values) {
    perror("malloc");
    exit(EXIT_FAILURE);
}

for (size_t i = 0; i < 10; ++i)
    values[i] = (int)(i + 1);

for (size_t i = 0; i < 10; ++i)
    printf("%d ", values[i]);

free(values);
values = NULL;          /* defensive programming */

Passing 0 yields implementation-defined behaviour; do not rely on it.

calloc

void *calloc(size_t count, size_t size);

Allocates space for count objects of size bytes each and zeroes the entire block. Otherwise behaves like malloc.

double *matrix = calloc(4, sizeof *matrix);   /* four zeroed doubles */
free(matrix);

realloc

void *realloc(void *ptr, size_t new_size);
  • If ptr is NULL, behaves like malloc(new_size).
  • If new_size is zero, behaviour is implementation-defined (acts like free(ptr) on many systems).
  • Otherwise it tries to resize the existing block:
    • In-place: when the adjacent heap space is free, the same address is returned.
    • Move-and-copy: when no room, a new block is allocated, data is copied, the old block is freed, and the new address is returned.

Always capture the return value in a temporary pointer to avoid leaks on failure:

size_t cap = 5;
int *data = malloc(cap * sizeof *data);
...
if (need_more) {
    cap *= 2;
    int *tmp = realloc(data, cap * sizeof *data);
    if (!tmp) {
        perror("realloc");
        free(data);
        exit(EXIT_FAILURE);
    }
    data = tmp;
}

free

void free(void *ptr);

Releases a block previously obtained from malloc, calloc, or realloc. Passing NULL is harmless; anything else invokes undefined behaviour.

Typical Heap Mistakes

Fault Minimal Example Remedy
Dereferencing NULL int *p = malloc(SIZE_MAX); *p = 3; Check result before use.
Out-of-bounds write int *p = malloc(5 * sizeof *p); p[5] = 0; Allocate enough bytes, use correct indices.
free on non-heap pointer int x; free(&x); Only free heap blocks.
Partial free p += 10; free(p); Pass the original base address.
Double free free(p); free(p); Set pointer to NULL after first free.
Leak void f(){ malloc(100); } Ensure every path frees the block.

Quiz Walk-through

Question 1

void grab(char *p) {
    p = malloc(100);
}
void demo(void) {
    char *s = NULL;
    grab(s);                /* s unchanged */
    strcpy(s, "hi");        /* UB */
}

Fix: pass the address of the pointer.

void grab(char **p) { *p = malloc(100); }
...
grab(&s);
...
free(s);

Question 2

Returning the address of a local variable:

char *broken(void) {
    char buf[] = "hello";
    return buf;             /* dangling pointer */
}

Options:

const char *ok(void) {
    return "hello";          /* string literal lives in static segment */
}

static char buf[100];
char *also_ok(void) {
    strcpy(buf, "hello");
    return buf;              /* static storage */
}

Question 3

Memory leak variant:

void get(char **p, int n) { *p = malloc(n); }
void run(void) {
    char *s = NULL;
    get(&s, 100);
    puts("leaked");        /* forgot free */
}

Question 4

Use-after-free:

char *s = malloc(100);
free(s);
if (s) strcpy(s, "oops");  /* UB */

Nullify the pointer after freeing.

Process Memory Map

Segment Storage Duration Typical Contents
Stack automatic local variables, return addresses
Heap explicit blocks from malloc family
Data / BSS program lifetime globals, static locals
Code / Text program lifetime machine instructions, read-only constants
int g = 1;                 /* Data */
static int sg = 2;         /* Data */

void f(void) {
    static int s = 3;     /* Data */
    int l = 4;            /* Stack */
    int *h = malloc(sizeof *h);  /* Heap */
    free(h);
}

Flexible Array Members (C99)

A structure may end with an array of unknown size:

typedef struct msg {
    uint32_t len;
    char payload[];   /* flexible */
} msg_t;
  • sizeof(msg_t) ignores the flexible member.
  • At least one other member must precede it.
  • Allocate extra bytes for the tail:
msg_t *m = malloc(sizeof *m + needed);
if (!m) { perror("malloc"); exit(EXIT_FAILURE); }
m->len = needed;
/* use m->payload[i] ... */
free(m);

Advantages over a separate pointer:

  1. One malloc / one free simplifies ownership.
  2. Contiguous layout improves locality and reduces fragmentation.

Alternative with pointer:

typedef struct msg_alt {
    uint32_t len;
    char *payload;
} msg_alt_t;

msg_alt_t *m = malloc(sizeof *m);
m->payload = malloc(needed);
...
free(m->payload);
free(m);

Requires two allocations and two releases, and the two heap blocks may be far apart.


Always pair every successful allocation with a matching free, guard against NULL, and never access memory after it has been released.

Tags: C memory-management malloc calloc realloc

Posted on Sat, 01 Aug 2026 16:29:43 +0000 by junrey