C dynamic memory allocation with malloc calloc realloc and free explained

C Dynamic Memory Allocation: malloc, calloc, realloc & free Explained

Why Dynamic Memory?

Every C program you have written so far used stack memory. You declared a variable, the compiler reserved space, and that space vanished when the function returned. Simple, automatic, fast — but limited. Stack memory has a fixed size (typically 1–8 MB), and every allocation must happen at compile time. What if you need an array whose size depends on user input? What if you need memory that outlives the function that created it?

That is where C dynamic memory allocation enters. The heap is a large pool of memory your program can tap into at runtime. You ask the operating system for bytes, use them, and hand them back when done. Four functions from <stdlib.h> control this process: malloc, calloc, realloc, and free.

#include <stdlib.h>

// Stack allocation — size must be known at compile time
int stack_arr[100];

// Heap allocation — size can be determined at runtime
int n = get_user_input();
int *heap_arr = malloc(n * sizeof(int));  // runtime size

Think of stack memory like a fixed-size notebook — you get a certain number of pages and that is it. Heap memory is like a library: you borrow books (memory) when you need them and return them when done. The library is much bigger, but you are responsible for returning what you borrowed.

malloc — Memory Allocation

malloc stands for memory allocation. It allocates a contiguous block of bytes on the heap and returns a pointer to the first byte. The memory is uninitialized — it contains whatever garbage was there before.

void *malloc(size_t size);
// Returns: pointer to allocated memory, or NULL on failure

The return type is void *, a generic pointer that you cast (or let C implicitly convert) to the type you need:

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

int main(void) {
    // Allocate space for 5 integers
    int *arr = malloc(5 * sizeof(int));

    // ALWAYS check if malloc succeeded
    if (arr == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    // Use the memory
    for (int i = 0; i < 5; i++) {
        arr[i] = (i + 1) * 10;
    }

    // Print values
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    // Output: arr[0]=10, arr[1]=20, arr[2]=30, arr[3]=40, arr[4]=50

    // Free the memory when done
    free(arr);
    return 0;
}

Critical rule: Always check the return value. malloc returns NULL if the system cannot satisfy your request. Dereferencing NULL is undefined behavior — your program will crash (if you are lucky) or corrupt data silently (if you are not).

Why sizeof Matters

Never hardcode byte sizes. An int might be 4 bytes on your machine but 2 bytes on an embedded system. Always use sizeof:

// Bad — assumes int is 4 bytes
int *p = malloc(20);

// Good — portable across all platforms
int *p = malloc(5 * sizeof(int));

// Even better — tied to the variable itself
int *p = malloc(5 * sizeof(*p));

The last form, sizeof(*p), is the gold standard. If you change p from int * to double *, the allocation automatically adjusts. This pattern prevents entire categories of bugs.

calloc — Contiguous Allocation

calloc allocates memory for an array of elements and zero-initializes every byte. It takes two arguments instead of one:

void *calloc(size_t count, size_t size);
// Allocates count * size bytes, all set to zero
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // Allocate 5 integers, all initialized to 0
    int *arr = calloc(5, sizeof(int));

    if (arr == NULL) {
        fprintf(stderr, "Allocation failed\n");
        return 1;
    }

    // All values are guaranteed to be 0
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] = %d\n", i, arr[i]);
    }
    // Output: 0, 0, 0, 0, 0

    free(arr);
    return 0;
}

malloc vs calloc

Feature malloc calloc
Arguments Total bytes Count + element size
Initialization Uninitialized (garbage) Zero-initialized
Speed Slightly faster Slightly slower (zeroing)
Overflow check None Checks count × size overflow
Use when You will set values immediately You need zeros or safety

calloc has a hidden safety benefit: it checks for integer overflow in count * size. If the multiplication overflows, calloc returns NULL instead of allocating a tiny buffer. With malloc, you could accidentally allocate far less than intended.

realloc — Resize Allocation

realloc changes the size of a previously allocated block. It can grow or shrink memory:

void *realloc(void *ptr, size_t new_size);
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // Start with 3 integers
    int *arr = malloc(3 * sizeof(int));
    if (!arr) return 1;

    arr[0] = 10;
    arr[1] = 20;
    arr[2] = 30;

    // Need more space — grow to 5 integers
    int *temp = realloc(arr, 5 * sizeof(int));
    if (temp == NULL) {
        // realloc failed — original arr is still valid!
        fprintf(stderr, "Realloc failed\n");
        free(arr);
        return 1;
    }
    arr = temp;  // Safe to reassign now

    arr[3] = 40;
    arr[4] = 50;

    for (int i = 0; i < 5; i++) {
        printf("%d ", arr[i]);
    }
    // Output: 10 20 30 40 50

    free(arr);
    return 0;
}

The golden rule of realloc: Never write arr = realloc(arr, new_size) directly. If realloc fails, it returns NULL, and you have just overwritten your only pointer to the original memory — creating an unrecoverable memory leak. Always use a temporary pointer.

How realloc Works Internally

When you call realloc, the allocator tries three strategies in order:

  1. Expand in place — If there is free space after the current block, simply extend it. Fastest case.
  2. Allocate + copy + free — Allocate a new block, copy old data, free the old block. Slower but necessary when the block cannot grow.
  3. Fail — Return NULL if no memory is available. The original block remains untouched.

free — Release Memory

free returns heap memory to the operating system (or the allocator’s free list). Every malloc, calloc, or realloc must have a matching free:

void free(void *ptr);
int *data = malloc(100 * sizeof(int));
// ... use data ...
free(data);      // Release the memory
data = NULL;     // Prevent dangling pointer

Setting the pointer to NULL after free is a defensive practice. If you accidentally use the pointer later, dereferencing NULL gives a clear crash instead of silent corruption.

Rules for free

  • Only free memory returned by malloc/calloc/realloc
  • Never free the same pointer twice (double free — undefined behavior)
  • free(NULL) is safe and does nothing — no need to check before calling
  • Do not use memory after freeing it (use-after-free)

Common Patterns

Dynamic Array (Growable Array)

The most common use of dynamic memory — an array that grows as needed:

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

typedef struct {
    int *data;
    int size;
    int capacity;
} DynArray;

DynArray *dynarray_create(int initial_cap) {
    DynArray *da = malloc(sizeof(DynArray));
    if (!da) return NULL;

    da->data = malloc(initial_cap * sizeof(int));
    if (!da->data) {
        free(da);
        return NULL;
    }

    da->size = 0;
    da->capacity = initial_cap;
    return da;
}

int dynarray_push(DynArray *da, int value) {
    if (da->size == da->capacity) {
        int new_cap = da->capacity * 2;  // Double the capacity
        int *temp = realloc(da->data, new_cap * sizeof(int));
        if (!temp) return -1;  // Failure

        da->data = temp;
        da->capacity = new_cap;
    }

    da->data[da->size++] = value;
    return 0;
}

void dynarray_free(DynArray *da) {
    if (da) {
        free(da->data);
        free(da);
    }
}

int main(void) {
    DynArray *arr = dynarray_create(4);

    for (int i = 0; i < 20; i++) {
        dynarray_push(arr, i * 10);
    }

    printf("Size: %d, Capacity: %d\n", arr->size, arr->capacity);
    // Output: Size: 20, Capacity: 32

    for (int i = 0; i < arr->size; i++) {
        printf("%d ", arr->data[i]);
    }

    dynarray_free(arr);
    return 0;
}

This doubling strategy gives amortized O(1) insertion. It is exactly how std::vector works in C++ and ArrayList in Java.

Dynamic String

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

char *read_line(void) {
    int capacity = 16;
    int length = 0;
    char *buffer = malloc(capacity);
    if (!buffer) return NULL;

    int ch;
    while ((ch = getchar()) != '\n' && ch != EOF) {
        if (length + 1 >= capacity) {
            capacity *= 2;
            char *temp = realloc(buffer, capacity);
            if (!temp) {
                free(buffer);
                return NULL;
            }
            buffer = temp;
        }
        buffer[length++] = (char)ch;
    }

    buffer[length] = '\0';
    return buffer;  // Caller must free
}

int main(void) {
    printf("Enter a line: ");
    char *line = read_line();

    if (line) {
        printf("You entered: %s\n", line);
        free(line);
    }

    return 0;
}

Common Mistakes

1. Forgetting to Free (Memory Leak)

void process(void) {
    int *data = malloc(1000 * sizeof(int));
    // ... use data ...
    return;  // BUG: data is never freed!
    // Every call leaks 4000 bytes
}

2. Using Memory After Free

int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p);  // BUG: use-after-free, undefined behavior

3. Double Free

int *p = malloc(sizeof(int));
free(p);
free(p);  // BUG: double free, undefined behavior

4. Freeing Stack Memory

int x = 42;
free(&x);  // BUG: x is on the stack, not the heap

5. Lost Pointer from realloc

int *arr = malloc(10 * sizeof(int));
arr = realloc(arr, 20 * sizeof(int));  // If realloc fails, arr becomes NULL
// Original memory is leaked forever

Best Practices

After years of debugging C programs, experienced developers follow these rules religiously:

  1. Always check return valuesmalloc, calloc, and realloc can all return NULL
  2. Use sizeof(*ptr) — Ties allocation to the pointer type automatically
  3. Set pointers to NULL after free — Prevents use-after-free and double-free
  4. Use realloc through a temp pointer — Prevents memory leaks on failure
  5. Match every allocation with a free — Write free immediately after malloc, then add code between
  6. Use Valgrind — Run valgrind --leak-check=full ./program to catch leaks
  7. Document ownership — Comment who is responsible for freeing each allocation
// Ownership pattern — caller owns the returned memory
char *create_greeting(const char *name) {
    // Caller must free the returned string
    char *msg = malloc(strlen(name) + 10);
    if (!msg) return NULL;
    sprintf(msg, "Hello, %s!", name);
    return msg;
}

Practice Exercises

Exercise 1: Dynamic Integer Array

Write a program that reads integers from the user until they enter -1. Store all numbers in a dynamically growing array, then print them in reverse order. Start with a capacity of 4 and double when full.

Exercise 2: String Concatenation

Write a function char *concat_all(const char **strings, int count) that takes an array of strings and returns one big string with all of them joined by spaces. The caller must free the result.

Exercise 3: Matrix Allocation

Write functions to dynamically allocate and free a 2D matrix of integers. Use malloc for an array of row pointers, then malloc each row separately. Include a function to fill the matrix with multiplication table values and print it.

Dynamic memory is the foundation of every serious C program — from string handling to data structures to operating systems. Master malloc and free now, and you will understand how every higher-level language manages memory under the hood. In the next lesson, we will peek behind the curtain to see exactly where stack and heap memory live in your program’s address space.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *