Skip to content
Home » Embedded Systems » Embedded C » Dynamic Memory Allocation in C

Dynamic Memory Allocation in C

Dynamic Memory Allocation in C featured image with dark blue background, C FOUNDATIONS badge, curly braces icon in teal circle, and malloc calloc realloc and free subtitle by nerdyelectronics.com
RTOS for Embedded Systems
Part 9 of 10View Full Path →

KEY TAKEAWAYS

  • Dynamic memory allocation (malloc, calloc, realloc, free) manages memory at runtime
  • Always check the return value of malloc() for NULL before using the allocated memory
  • Every malloc() must have a matching free() to prevent memory leaks
  • Dynamic allocation is generally avoided in embedded systems due to fragmentation and determinism concerns

What is Dynamic Memory Allocation?

In C programming, memory can be allocated in two ways: statically (at compile time) and dynamically (at run time). When you declare a variable like int x; or an array like int arr[10];, the compiler decides how much memory to reserve before the program even runs. This is called static memory allocation.But what if you don’t know how many elements you need until the program is running? That is where dynamic memory allocation comes in. It allows you to request memory from the operating system at run time and release it when you no longer need it.Dynamic memory is allocated from a region called the heap, which is different from the stack where local variables live.

Stack vs Heap

Understanding the difference between stack and heap is essential before diving into dynamic allocation.
FeatureStackHeap
AllocationAutomatic (compile time)Manual (run time)
DeallocationAutomatic (when function returns)Manual (you must call free)
SpeedVery fastSlower
SizeLimited (typically a few KB)Much larger (depends on system)
ScopeLocal to the functionAccessible until freed
FragmentationNoPossible
In embedded systems, stack size is often very limited (sometimes just 1-4 KB), making dynamic allocation a useful but careful choice.

The Four Functions for Dynamic Memory

C provides four standard library functions in <stdlib.h> for dynamic memory management:

1. malloc() – Memory Allocation

malloc() allocates a block of memory of the specified size in bytes. The memory is not initialized, meaning it contains garbage values.Syntax:
void *malloc(size_t size);
Example:
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *ptr;
    int n = 5;

    // Allocate memory for 5 integers
    ptr = (int *)malloc(n * sizeof(int));

    if (ptr == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }

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

    // Print values
    for (int i = 0; i < n; i++) {
        printf("ptr[%d] = %d\n", i, ptr[i]);
    }

    // Free the memory
    free(ptr);

    return 0;
}
Output:
ptr[0] = 0
ptr[1] = 10
ptr[2] = 20
ptr[3] = 30
ptr[4] = 40

2. calloc() – Contiguous Allocation

calloc() works like malloc but takes two parameters: the number of elements and the size of each element. The key difference is that calloc initializes all bytes to zero.Syntax:
void *calloc(size_t num, size_t size);
Example:
int *ptr = (int *)calloc(5, sizeof(int));

if (ptr == NULL) {
    printf("Memory allocation failed!\n");
    return 1;
}

// All values are initialized to 0
for (int i = 0; i < 5; i++) {
    printf("ptr[%d] = %d\n", i, ptr[i]);  // prints 0 for all
}

free(ptr);

3. realloc() – Re-Allocation

realloc() changes the size of a previously allocated memory block. It can grow or shrink the block. If it grows and there is not enough contiguous space, it allocates a new block, copies the old data, and frees the old block.Syntax:
void *realloc(void *ptr, size_t new_size);
Example:
int *ptr = (int *)malloc(3 * sizeof(int));
ptr[0] = 10;
ptr[1] = 20;
ptr[2] = 30;

// Now we need space for 5 integers
ptr = (int *)realloc(ptr, 5 * sizeof(int));

if (ptr == NULL) {
    printf("Reallocation failed!\n");
    return 1;
}

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

// Old values are preserved
for (int i = 0; i < 5; i++) {
    printf("ptr[%d] = %d\n", i, ptr[i]);
}

free(ptr);
Important: Never do ptr = realloc(ptr, new_size); in production code without a temporary pointer. If realloc fails, it returns NULL and you lose the reference to the original block, causing a memory leak. The safe pattern is:
int *temp = (int *)realloc(ptr, new_size);
if (temp == NULL) {
    // handle error, ptr is still valid
    free(ptr);
    return 1;
}
ptr = temp;

4. free() – Deallocate Memory

free() releases memory that was previously allocated with malloc, calloc, or realloc. After freeing, the pointer becomes a dangling pointer and should be set to NULL.Syntax:
void free(void *ptr);
Best practice:
free(ptr);
ptr = NULL;  // Prevent dangling pointer

Common Mistakes and Pitfalls

1. Memory Leak

A memory leak occurs when allocated memory is never freed. In long-running embedded systems, this can eventually exhaust all available memory.
void bad_function() {
    int *ptr = (int *)malloc(100 * sizeof(int));
    // ... use ptr ...
    return;  // Memory leaked! Never freed.
}

2. Dangling Pointer

Accessing memory after it has been freed leads to undefined behavior.
int *ptr = (int *)malloc(sizeof(int));
*ptr = 42;
free(ptr);
// ptr still holds the old address
*ptr = 10;  // DANGEROUS: undefined behavior

3. Double Free

Calling free on the same pointer twice causes undefined behavior and potential crashes.
free(ptr);
free(ptr);  // DANGEROUS: double free

4. Not Checking for NULL

malloc and calloc return NULL if allocation fails. Always check the return value.

Dynamic Memory in Embedded Systems

In embedded systems, dynamic memory allocation is a topic of debate. Here is why:Reasons to avoid it:
  • Fragmentation: Repeated allocation and freeing can fragment the heap, making large allocations impossible even when total free memory is sufficient
  • Non-deterministic timing: malloc may take variable time, which is problematic in real-time systems
  • Limited heap: Microcontrollers often have very limited RAM (sometimes just a few KB)
  • No memory protection: Many microcontrollers lack an MMU, so a heap overflow corrupts other memory
When it makes sense:
  • During initialization only (allocate once, never free)
  • When the data size is truly unknown at compile time
  • On larger embedded platforms (Raspberry Pi, ESP32) where RAM is more abundant
  • Using a custom memory pool allocator with fixed-size blocks

A Practical Example: Dynamic Array of Sensor Readings

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

typedef struct {
    float temperature;
    float humidity;
    unsigned long timestamp;
} SensorReading;

int main() {
    int capacity = 4;
    int count = 0;
    SensorReading *readings;

    // Start with space for 4 readings
    readings = (SensorReading *)malloc(capacity * sizeof(SensorReading));
    if (readings == NULL) {
        printf("Initial allocation failedn");
        return 1;
    }

    // Simulate receiving sensor data
    float temps[] = {23.5, 24.1, 22.8, 25.0, 24.3, 23.9};
    float humids[] = {45.0, 46.2, 44.8, 47.1, 45.5, 46.0};
    int num_samples = 6;

    for (int i = 0; i < num_samples; i++) {
        // Grow the array if needed
        if (count == capacity) {
            capacity *= 2;
            SensorReading *temp = (SensorReading *)realloc(readings,
                                   capacity * sizeof(SensorReading));
            if (temp == NULL) {
                printf("Reallocation failedn");
                free(readings);
                return 1;
            }
            readings = temp;
            printf("Array grown to capacity: %d\n", capacity);
        }

        readings[count].temperature = temps[i];
        readings[count].humidity = humids[i];
        readings[count].timestamp = 1000 + (i * 5000);
        count++;
    }

    // Print all readings
    printf("\nSensor Readings:\n");
    for (int i = 0; i < count; i++) {
        printf("  [%lu] Temp: %.1f C, Humidity: %.1f%%\n",
               readings[i].timestamp,
               readings[i].temperature,
               readings[i].humidity);
    }

    free(readings);
    readings = NULL;

    return 0;
}

Summary

FunctionPurposeInitializes Memory?
malloc()Allocate a block of memoryNo (garbage values)
calloc()Allocate and zero-initializeYes (all zeros)
realloc()Resize an existing blockOnly new portion is uninitialized
free()Release allocated memoryN/A
Dynamic memory allocation gives your C programs flexibility to handle data of unknown size at run time. However, in embedded systems, use it carefully due to fragmentation, limited RAM, and real-time constraints. Always check for NULL, always free what you allocate, and consider static allocation or memory pools as alternatives when determinism matters.

Related on this site

Leave a Reply

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