C Dynamic Memory Allocation

Dynamic memory allocation allows a C program to request memory at runtime instead of determining the complete storage requirement at compile time. The standard library provides malloc, calloc, realloc, and free for managing dynamically allocated storage.

Stack and Heap Memory

Automatic local variables are commonly stored in stack-managed storage, while memory obtained through malloc, calloc, or realloc comes from dynamically allocated storage. The exact implementation of these storage areas is platform-dependent, but the distinction is useful when discussing C memory management.

Why Dynamic Allocation Is Useful

  • Allocate memory based on runtime input
  • Create dynamically sized arrays
  • Build linked data structures
  • Resize buffers as data grows
  • Manage objects whose lifetime extends beyond a single block
  • Avoid unnecessarily large fixed-size arrays

malloc

malloc allocates a specified number of bytes and returns a pointer to the allocated storage. The contents of newly allocated memory are indeterminate.

C
#include <stdlib.h>

int *values = malloc(10 * sizeof *values);

if (values == NULL) {
    return EXIT_FAILURE;
}

Using sizeof with malloc

Using sizeof on the pointed-to object makes allocation expressions easier to maintain when the pointer type changes.

C
int *values = malloc(10 * sizeof *values);

malloc Does Not Initialize Memory

The bytes returned by malloc have indeterminate values. Reading an object before storing an appropriate value into it can result in undefined behavior.

C
int *value = malloc(sizeof *value);

if (value != NULL) {
    *value = 42;
}

calloc

calloc allocates storage for an array of objects and initializes all bytes of the allocated storage to zero.

C
int *values = calloc(10, sizeof *values);

if (values == NULL) {
    return EXIT_FAILURE;
}

malloc vs calloc

FunctionPurposeInitialization
mallocAllocate a specified number of bytesNo initialization of allocated storage
callocAllocate an array of objectsAll allocated bytes are initialized to zero

free

free releases dynamically allocated storage back to the implementation.

C
int *values = malloc(10 * sizeof *values);

if (values != NULL) {
    /* use values */
    free(values);
}

A Pointer After free

Calling free does not automatically change the pointer variable to NULL. The pointer becomes invalid for accessing the released object.

C
free(values);
values = NULL;

Setting the pointer to NULL can prevent accidental reuse of that particular pointer variable.

free(NULL)

Calling free with a null pointer has no effect.

C
int *values = NULL;
free(values);

realloc

realloc changes the size of a previously allocated object. It may extend the existing allocation or move the object to a new location.

C
int *values = malloc(10 * sizeof *values);

if (values == NULL) {
    return EXIT_FAILURE;
}

int *tmp = realloc(values, 20 * sizeof *values);

if (tmp == NULL) {
    free(values);
    return EXIT_FAILURE;
}

values = tmp;

Why realloc Should Use a Temporary Pointer

If realloc fails, it returns NULL while the original allocation remains valid. Assigning realloc directly to the only pointer would lose the original address and cause a memory leak.

C
int *tmp = realloc(values, new_count * sizeof *values);

if (tmp != NULL) {
    values = tmp;
}

realloc with Size Zero

Requests involving a zero size have special implementation-defined or implementation-specific historical behavior depending on the C standard version and implementation. Portable code should avoid relying on realloc(ptr, 0) as a substitute for an explicit free.

Growing a Dynamic Array

C
#include <stdlib.h>

int main(void)
{
    size_t capacity = 4;
    size_t count = 0;
    int *values = malloc(capacity * sizeof *values);

    if (values == NULL) {
        return EXIT_FAILURE;
    }

    for (int i = 0; i < 20; ++i) {
        if (count == capacity) {
            size_t new_capacity = capacity * 2;
            int *tmp = realloc(values,
                               new_capacity * sizeof *values);

            if (tmp == NULL) {
                free(values);
                return EXIT_FAILURE;
            }

            values = tmp;
            capacity = new_capacity;
        }

        values[count++] = i;
    }

    free(values);
    return EXIT_SUCCESS;
}

Allocation Failure

malloc, calloc, and realloc can fail to allocate the requested storage. A failed allocation returns NULL, so programs must check the result before dereferencing it.

C
char *buffer = malloc(1024);

if (buffer == NULL) {
    /* Handle allocation failure */
    return EXIT_FAILURE;
}

Integer Overflow in Allocation Sizes

Calculating the number of bytes with multiplication can overflow size_t before the allocation function is called.

C
size_t count = 1000000;
size_t element_size = sizeof(int);

if (count > SIZE_MAX / element_size) {
    return EXIT_FAILURE;
}

int *values = malloc(count * element_size);

For allocation calculations involving untrusted or very large sizes, check multiplication overflow before calling an allocation function.

Dynamic Arrays

A dynamically allocated block can be accessed using normal array indexing because allocated storage can be used through a pointer to the appropriate object type.

C
size_t count = 100;
int *values = malloc(count * sizeof *values);

if (values != NULL) {
    values[0] = 10;
    values[99] = 20;

    free(values);
}

Dynamic Strings

Dynamically allocated storage is useful when a string's required size is determined at runtime.

C
const char *source = "Hello, C!";

size_t length = strlen(source);
char *copy = malloc(length + 1);

if (copy != NULL) {
    memcpy(copy, source, length + 1);
    free(copy);
}

The additional byte is required for the terminating null character.

Including string.h

C
#include <string.h>

Memory Ownership

Every dynamically allocated object should have clearly defined ownership: code should know who is responsible for eventually calling free.

Ownership Transfer

A function can transfer ownership of dynamically allocated memory to its caller.

C
char *create_message(void)
{
    char *message = malloc(32);

    if (message == NULL) {
        return NULL;
    }

    strcpy(message, "Hello");
    return message;
}

/* Caller owns the returned memory. */

Memory Leaks

A memory leak occurs when dynamically allocated storage is no longer reachable but has not been released.

C
int *values = malloc(100 * sizeof *values);

/* Pointer is lost without calling free. */
values = NULL;

Repeated leaks can cause a program's memory usage to grow unnecessarily.

Dangling Pointers

A dangling pointer refers to storage whose lifetime has ended.

C
int *value = malloc(sizeof *value);

if (value != NULL) {
    *value = 42;
    free(value);

    /* value is now a dangling pointer. */
}

Use-After-Free

Accessing an object after its allocated storage has been released results in undefined behavior.

C
free(value);

/* Undefined behavior. */
printf("%d\n", *value);

Double Free

Passing the same allocation to free more than once results in undefined behavior.

C
free(value);
free(value); /* Undefined behavior */

Invalid free

Only pointers returned by the appropriate allocation functions, or a null pointer, may be passed to free. A pointer to an automatic object must not be freed.

C
int value = 42;

free(&value); /* Invalid */

Interior Pointers

A pointer to the middle of an allocated block is not itself a pointer that can be passed to free.

C
char *buffer = malloc(100);

if (buffer != NULL) {
    char *middle = buffer + 10;

    free(middle); /* Invalid */
    free(buffer);
}

Matching Allocation and Deallocation

Memory obtained from malloc, calloc, or realloc is released with free. C does not provide a separate delete operator like C++.

realloc Can Move Memory

After a successful realloc that changes the object's address, pointers into the old allocation become invalid because the old storage is released.

C
int *values = malloc(10 * sizeof *values);

if (values != NULL) {
    int *element = &values[3];

    int *tmp = realloc(values, 100 * sizeof *values);

    if (tmp != NULL) {
        values = tmp;
        /* element may now be invalid. */
    }
}

Avoiding Stale Pointers After realloc

After a successful realloc, recompute pointers into the allocation instead of assuming previous interior pointers remain valid.

Dynamic Structures

Dynamic allocation is fundamental for structures such as linked lists, trees, graphs, hash tables, and dynamically sized buffers.

C
struct Node {
    int value;
    struct Node *next;
};

struct Node *node = malloc(sizeof *node);

if (node != NULL) {
    node->value = 42;
    node->next = NULL;
}

Allocating a Struct Correctly

C
struct Node *node = malloc(sizeof *node);

Using sizeof *node avoids repeating the structure type in the allocation expression.

Freeing a Linked Structure

C
struct Node *current = head;

while (current != NULL) {
    struct Node *next = current->next;
    free(current);
    current = next;
}

Nested Allocations

If a dynamically allocated object owns other dynamically allocated objects, all owned allocations must eventually be released.

C
struct Person {
    char *name;
};

struct Person *person = malloc(sizeof *person);

if (person != NULL) {
    person->name = malloc(32);

    if (person->name == NULL) {
        free(person);
        return EXIT_FAILURE;
    }

    free(person->name);
    free(person);
}

Partial Allocation Failure

When several allocations are required to construct one object, failure partway through construction requires cleanup of everything that was already allocated.

Cleanup Pattern

C
char *first = NULL;
char *second = NULL;

first = malloc(100);
if (first == NULL) {
    return EXIT_FAILURE;
}

second = malloc(200);
if (second == NULL) {
    free(first);
    return EXIT_FAILURE;
}

/* Use both allocations. */

free(second);
free(first);

calloc and Zero Values

calloc initializes allocated bytes to zero. For integer types, an all-bits-zero representation is guaranteed to represent zero for the standard integer types. For arbitrary pointer or floating-point objects, do not generally assume that zeroing raw bytes is equivalent to assigning the C value 0.

Flexible Array Members

A flexible array member can be used with dynamic allocation to store a structure and additional variable-sized data in one allocation.

C
struct Buffer {
    size_t length;
    char data[];
};

size_t length = 100;
struct Buffer *buffer = malloc(sizeof *buffer + length);

if (buffer != NULL) {
    buffer->length = length;
    free(buffer);
}

Flexible Array Member Overflow

When calculating the allocation size for a flexible array member, check arithmetic for overflow before performing the allocation.

Alignment

Memory returned by the standard allocation functions is suitably aligned for any object type with a fundamental alignment requirement.

Allocated Memory and Lifetime

The lifetime of allocated storage begins when allocation succeeds and ends when the storage is released or otherwise becomes unavailable according to the applicable allocation operation.

Allocation Is Not Garbage Collection

C does not automatically determine when dynamically allocated objects are no longer needed. The programmer or an explicit ownership mechanism must ensure that allocated memory is released.

Memory Management APIs Beyond the C Standard

Operating systems and runtime libraries often provide additional allocation facilities. Examples include aligned allocation interfaces and debugging allocators. These APIs are platform-specific and should not be confused with the ISO C allocation functions.

Aligned Allocation

Modern C standards provide aligned_alloc for requesting storage with a specified alignment, subject to its size and alignment requirements.

C
#include <stdlib.h>

void *memory = aligned_alloc(64, 1024);

if (memory != NULL) {
    /* use memory */
    free(memory);
}

Memory Debugging

Dynamic-memory bugs can be difficult to diagnose because their effects may appear long after the original mistake. Memory debugging tools can detect leaks, invalid accesses, use-after-free errors, and double frees.

Common Dynamic Memory Bugs

  • Memory leaks
  • Use-after-free
  • Double free
  • Freeing a non-allocated pointer
  • Buffer overflows
  • Integer overflow in allocation sizes
  • Losing the original pointer after realloc failure
  • Using stale pointers after realloc moves an allocation
  • Forgetting to free nested allocations
  • Dereferencing NULL after allocation failure

Best Practices

  • Check every allocation that can fail
  • Use sizeof *pointer for allocation sizes
  • Use a temporary pointer with realloc
  • Define ownership clearly
  • Free each allocation exactly once
  • Set pointers to NULL when doing so improves safety
  • Check arithmetic for allocation-size overflow
  • Clean up partially constructed objects on failure
  • Avoid keeping stale interior pointers across realloc
  • Use memory-analysis tools during development
  • Keep allocation and deallocation responsibilities easy to identify

Quick Reference

FunctionPurpose
mallocAllocate a specified number of bytes
callocAllocate an array and zero all allocated bytes
reallocResize an existing allocation
freeRelease dynamically allocated storage
aligned_allocAllocate storage with a requested alignment

Practice Exercises

  • Allocate an integer array whose size is entered by the user
  • Initialize a dynamic array using calloc
  • Resize an array with realloc as new values are added
  • Write a function that dynamically creates a string copy
  • Implement a dynamically growing character buffer
  • Create and destroy a linked list using dynamic allocation
  • Handle allocation failure during construction of a nested structure
  • Demonstrate the difference between a memory leak and a dangling pointer
  • Write an allocation-size overflow check
  • Implement a structure containing a flexible array member
  • Track ownership of dynamically allocated objects in a small program
  • Use a memory debugging tool to find a deliberate leak

Conclusion

Dynamic memory allocation gives C programs control over storage whose size and lifetime are determined at runtime. malloc, calloc, realloc, and free form the core of standard C dynamic memory management.

That control comes with responsibility. Correct programs must handle allocation failures, prevent leaks and dangling pointers, avoid double frees and buffer overflows, and establish clear ownership rules. Good allocation patterns make dynamic C programs significantly safer and easier to maintain.