Home C / C++ Dynamic Arrays in C: malloc, realloc and resize patterns explained

Dynamic Arrays in C: malloc, realloc and resize patterns explained

In Plain English 🔥
Imagine you're setting up chairs for a party but you don't know how many guests are coming. A normal array is like pre-booking exactly 10 chairs — if 15 people show up, you're stuck. A dynamic array is like having a warehouse next door: you start with 10 chairs, and the moment you run out, you grab more from the warehouse and rearrange the room. That rearranging is exactly what realloc does behind the scenes — it finds you a bigger block of memory and moves everything over.
⚡ Quick Answer
Imagine you're setting up chairs for a party but you don't know how many guests are coming. A normal array is like pre-booking exactly 10 chairs — if 15 people show up, you're stuck. A dynamic array is like having a warehouse next door: you start with 10 chairs, and the moment you run out, you grab more from the warehouse and rearrange the room. That rearranging is exactly what realloc does behind the scenes — it finds you a bigger block of memory and moves everything over.

Every non-trivial C program eventually hits the same wall: you need to store a collection of things, but you have no idea at compile time how many things there'll be. Maybe you're reading lines from a log file, collecting sensor readings, or building a search result list. Static arrays — the ones declared with a fixed size like int scores[100] — force you to make a guess upfront, and that guess is almost always wrong. Too small and your program crashes or corrupts memory. Too large and you waste RAM that another process could have used.

Dynamic arrays solve this by shifting memory allocation from compile time to runtime. Instead of carving out space in the stack at the moment your function is called, you ask the operating system for heap memory exactly when you need it, in exactly the quantity you need. The heap is a large, flexible pool of memory your process can borrow from and return to at any time. This is the mechanism that powers virtually every data structure you'll ever use: vectors in C++, lists in Python, ArrayLists in Java — they all implement this same idea under the hood.

By the end of this article you'll know how to allocate a dynamic array with malloc, grow it safely with realloc using a doubling strategy, and release it properly with free. You'll also understand the common failure modes — memory leaks, dangling pointers, and the realloc trap — and exactly how to avoid them. Whether you're preparing for a systems programming interview or just trying to write C that doesn't blow up in production, this is the mental model you need.

Why malloc? Understanding Heap Allocation vs Stack Arrays

When you write int temperatures[50]; inside a function, C reserves exactly 200 bytes on the call stack the moment that function is entered. The stack is fast, automatic, and cleaned up when the function returns. But it has two hard limits: the size must be a compile-time constant (in standard C), and the memory vanishes the moment the function exits.

Heap allocation with malloc flips both of those constraints. You pass it a byte count at runtime — a value you can compute from user input, a file, or a loop — and it returns a pointer to a fresh block of memory. That block lives until you explicitly call free on it, regardless of which function is currently executing. This makes heap memory the right tool whenever you don't know size upfront, or when data needs to outlive the function that created it.

The cost is responsibility. The stack cleans itself up. The heap does not. If you forget to call free, that memory is gone for the lifetime of the process — that's a memory leak. If you call free and then keep using the pointer — that's a use-after-free bug, one of the most dangerous bugs in systems programming. Understanding this trade-off is the entire foundation of working with dynamic arrays in C.

heap_vs_stack.c · C
1234567891011121314151617181920212223242526272829303132333435363738394041
#include <stdio.h>
#include <stdlib.h>

// Demonstrates why stack arrays fail when size is runtime-determined
// and how malloc solves the problem cleanly.

int main(void) {
    int item_count;

    printf("How many temperature readings do you want to store? ");
    scanf("%d", &item_count);

    // STACK approach — ILLEGAL in standard C89 and risky even in C99:
    // int temperatures[item_count];  // <-- Variable Length Array, avoid in production

    // HEAP approach — safe, portable, works at any size:
    // malloc(n * sizeof(double)) asks the OS for exactly n doubles worth of bytes
    double *temperatures = malloc(item_count * sizeof(double));

    // malloc returns NULL if allocation fails (e.g., out of memory)
    // ALWAYS check — ignoring this causes a segfault on the next line
    if (temperatures == NULL) {
        fprintf(stderr, "Memory allocation failed. Cannot store %d readings.\n", item_count);
        return 1;  // exit with error code, not just 0
    }

    // Populate with dummy sensor readings
    for (int index = 0; index < item_count; index++) {
        temperatures[index] = 20.0 + (index * 0.5);  // simulate rising temps
    }

    // Use the data
    printf("\nStored %d readings. First: %.1f°C  Last: %.1f°C\n",
           item_count, temperatures[0], temperatures[item_count - 1]);

    // ALWAYS free heap memory when done — the OS won't do this for you
    free(temperatures);
    temperatures = NULL;  // null the pointer so it can't be accidentally used again

    return 0;
}
▶ Output
How many temperature readings do you want to store? 5

Stored 5 readings. First: 20.0°C Last: 22.0°C
⚠️
Watch Out: Never Skip the NULL Checkmalloc returns NULL when the system is out of memory. On embedded systems or heavily loaded servers this actually happens. Skipping the NULL check and proceeding to use the pointer causes a segmentation fault that's hard to reproduce and harder to debug. One if-block after every malloc is non-negotiable.

Growing a Dynamic Array with realloc — The Doubling Strategy

Here's the real heart of dynamic arrays: what happens when you've filled your allocated space and a new item arrives? You have two options. You could allocate a completely new block, copy everything over, and free the old one — which is exactly what realloc does for you in one function call. The question is not whether to use realloc, but how much to grow by.

Growing by one slot each time you're full sounds sensible but is catastrophically slow. If you're inserting 10,000 items, you trigger 10,000 reallocations, each potentially copying the entire array. That's O(n²) work for what should be O(n) insertions. The standard solution is capacity doubling: when full, double the capacity. This ensures that the total copying work across all insertions stays proportional to n — amortized O(1) per insert. This is the exact strategy used by C++ std::vector, Java ArrayList, and Python lists.

The realloc call itself has an important gotcha: if it fails, it returns NULL — but the original pointer is still valid and still holds your data. That's why you must store the result in a temporary pointer first, check for NULL, and only then overwrite your original pointer. Failing to do this is one of the most common memory bugs in C.

dynamic_array_grow.c · C
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// A reusable dynamic integer array with automatic growth.
// Models exactly how a C++ vector works internally.

typedef struct {
    int    *data;      // pointer to the heap block holding our integers
    int     count;     // how many items are currently stored
    int     capacity;  // how many items the current allocation can hold
} IntArray;

// Initialise the array with a small starting capacity
void array_init(IntArray *arr) {
    arr->capacity = 4;  // start small — we'll grow as needed
    arr->count    = 0;
    arr->data     = malloc(arr->capacity * sizeof(int));

    if (arr->data == NULL) {
        fprintf(stderr, "Failed to initialise dynamic array.\n");
        exit(1);
    }
}

// Append one integer, growing the backing array if necessary
void array_push(IntArray *arr, int value) {
    if (arr->count == arr->capacity) {
        // We're full — double the capacity
        int new_capacity = arr->capacity * 2;

        // Use a temporary pointer — if realloc fails we still have our old data
        int *resized = realloc(arr->data, new_capacity * sizeof(int));

        if (resized == NULL) {
            // realloc failed; arr->data is still valid, we just can't grow right now
            fprintf(stderr, "Could not grow array to capacity %d. Aborting push.\n", new_capacity);
            return;
        }

        // Safe to update now that we confirmed success
        arr->data     = resized;
        arr->capacity = new_capacity;

        printf("  [resize] Grew to capacity %d\n", new_capacity);
    }

    // Store the value and advance the count
    arr->data[arr->count] = value;
    arr->count++;
}

// Print every element with its index
void array_print(const IntArray *arr) {
    printf("Array (count=%d, capacity=%d): [", arr->count, arr->capacity);
    for (int i = 0; i < arr->count; i++) {
        printf("%d", arr->data[i]);
        if (i < arr->count - 1) printf(", ");
    }
    printf("]\n");
}

// Release heap memory — always call this when done
void array_free(IntArray *arr) {
    free(arr->data);
    arr->data     = NULL;  // prevent use-after-free
    arr->count    = 0;
    arr->capacity = 0;
}

int main(void) {
    IntArray scores;
    array_init(&scores);

    // Push 10 scores — watch the array resize automatically
    int game_scores[] = {42, 87, 15, 93, 56, 71, 34, 88, 62, 99};
    int total_games   = sizeof(game_scores) / sizeof(game_scores[0]);

    printf("Inserting %d scores into the dynamic array:\n", total_games);
    for (int i = 0; i < total_games; i++) {
        array_push(&scores, game_scores[i]);
    }

    printf("\nFinal state:\n");
    array_print(&scores);

    array_free(&scores);
    return 0;
}
▶ Output
Inserting 10 scores into the dynamic array:
[resize] Grew to capacity 8
[resize] Grew to capacity 16

Final state:
Array (count=10, capacity=16): [42, 87, 15, 93, 56, 71, 34, 88, 62, 99]
⚠️
Pro Tip: The Temporary Pointer is Not OptionalWriting arr->data = realloc(arr->data, new_size) is a memory leak waiting to happen. If realloc returns NULL, you've just overwritten your only pointer to the old block — that memory is now unreachable and unfreeable. Always realloc into a temporary, check it, then assign.

Shrinking, Searching and Removing — Real-World Array Operations

Growing an array grabs the headlines, but real programs also need to remove items and reclaim wasted space. If a user deletes half their entries, keeping a capacity of 10,000 slots for 50 items wastes significant RAM — especially on embedded hardware.

Shrinking follows the same pattern as growing but in reverse: when count drops below a threshold (a common choice is one quarter of capacity), realloc down to half of capacity. This keeps wasted space bounded without thrashing — if you shrank every single time you removed one element, you'd just end up reallocating immediately on the next insert.

Removing an element from the middle requires shifting every element after it one position to the left to fill the gap. This is O(n) in the worst case. If your workload involves many random removals, a linked list might be a better structure — but if removals are rare or always happen at the end, a dynamic array beats a linked list on cache performance because its elements are contiguous in memory. CPUs love contiguous data.

dynamic_array_remove.c · C
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct {
    int *data;
    int  count;
    int  capacity;
} IntArray;

void array_init(IntArray *arr) {
    arr->capacity = 8;
    arr->count    = 0;
    arr->data     = malloc(arr->capacity * sizeof(int));
    if (!arr->data) { fprintf(stderr, "Init failed\n"); exit(1); }
}

void array_push(IntArray *arr, int value) {
    if (arr->count == arr->capacity) {
        int  new_cap  = arr->capacity * 2;
        int *resized  = realloc(arr->data, new_cap * sizeof(int));
        if (!resized) { fprintf(stderr, "Grow failed\n"); return; }
        arr->data     = resized;
        arr->capacity = new_cap;
    }
    arr->data[arr->count++] = value;
}

// Remove the element at position `index`
// Shifts everything after it left by one slot
int array_remove(IntArray *arr, int index) {
    if (index < 0 || index >= arr->count) {
        fprintf(stderr, "Remove index %d out of bounds (count=%d)\n", index, arr->count);
        return -1;  // signal failure
    }

    int removed_value = arr->data[index];

    // Shift elements left — memmove handles overlapping regions safely
    memmove(
        &arr->data[index],          // destination: where the gap is
        &arr->data[index + 1],      // source: one past the gap
        (arr->count - index - 1) * sizeof(int)  // bytes to move
    );

    arr->count--;

    // Shrink if we're only using a quarter of our capacity
    // Floor at a minimum capacity of 4 to avoid tiny allocations
    if (arr->count > 0 && arr->count <= arr->capacity / 4 && arr->capacity > 4) {
        int  new_cap = arr->capacity / 2;
        int *shrunk  = realloc(arr->data, new_cap * sizeof(int));
        if (shrunk) {  // shrink is optional — if it fails, just keep the bigger block
            arr->data     = shrunk;
            arr->capacity = new_cap;
            printf("  [resize] Shrunk to capacity %d\n", new_cap);
        }
    }

    return removed_value;
}

void array_print(const IntArray *arr) {
    printf("[count=%d cap=%d]: ", arr->count, arr->capacity);
    for (int i = 0; i < arr->count; i++) printf("%d ", arr->data[i]);
    printf("\n");
}

void array_free(IntArray *arr) {
    free(arr->data);
    arr->data = NULL;
    arr->count = arr->capacity = 0;
}

int main(void) {
    IntArray task_ids;
    array_init(&task_ids);

    // Simulate a task queue with 8 tasks
    for (int id = 101; id <= 108; id++) {
        array_push(&task_ids, id);
    }

    printf("Initial queue:\n");
    array_print(&task_ids);

    // Remove task 103 (at index 2)
    int removed = array_remove(&task_ids, 2);
    printf("\nRemoved task ID %d\n", removed);
    array_print(&task_ids);

    // Remove several more to trigger shrink
    array_remove(&task_ids, 0);
    array_remove(&task_ids, 0);
    array_remove(&task_ids, 0);
    array_remove(&task_ids, 0);
    printf("\nAfter 4 more removals:\n");
    array_print(&task_ids);

    array_free(&task_ids);
    return 0;
}
▶ Output
Initial queue:
[count=8 cap=8]: 101 102 103 104 105 106 107 108

Removed task ID 103
[count=7 cap=8]: 101 102 104 105 106 107 108

After 4 more removals:
[resize] Shrunk to capacity 4
[count=3 cap=4]: 105 106 107
🔥
Why memmove and Not memcpy?memcpy has undefined behaviour when source and destination regions overlap. When you shift elements left inside the same array, the regions always overlap. memmove is guaranteed safe with overlapping regions — it handles the copy direction internally. Swap the two and you'll get subtle data corruption on some compilers and CPU architectures.
Feature / AspectStatic Array (stack)Dynamic Array (heap)
Size known at compile time?Required — must be a constantNot needed — set at runtime
Memory locationStack — automatic cleanupHeap — manual free required
Resize after creationImpossibleYes, via realloc
Access speed (indexing)O(1) — identicalO(1) — identical
Insert at middleImpossible after declarationO(n) — requires shifting
Append to end (amortised)N/A — fixed sizeO(1) — with doubling strategy
Risk of memory leakNone — stack auto-cleansYes — must call free
Risk of stack overflowYes — large arrays overflow stackNo — heap is much larger
Cache friendlinessExcellent — contiguousExcellent — contiguous
LifetimeUntil function returnsUntil free is called

🎯 Key Takeaways

  • malloc allocates on the heap at runtime — this is the only way to create arrays whose size depends on user input, file contents, or any value you don't know at compile time.
  • Always double capacity on growth, not increment by one — this keeps amortised insertion O(1) and is the strategy used by every major language's built-in list type.
  • realloc must go into a temporary pointer first — overwriting your original pointer directly causes an unrecoverable memory leak if the allocation fails.
  • After free, set the pointer to NULL immediately — stale non-NULL pointers that point to freed memory are one of the hardest bug classes to diagnose in C.

⚠ Common Mistakes to Avoid

  • Mistake 1: Writing arr->data = realloc(arr->data, new_size) directly — Symptom: if realloc returns NULL (out of memory), you've overwritten your only pointer to the old block, leaking it permanently with no way to recover or free it — Fix: always realloc into a separate temporary pointer, check it for NULL, then assign to arr->data only on success.
  • Mistake 2: Not nullifying a pointer after calling free — Symptom: a later code path checks 'if (ptr)' expecting NULL to mean unallocated, but the freed pointer still holds a non-NULL garbage address, so the check passes and you access deallocated memory causing silent data corruption or a crash — Fix: immediately after free(ptr), write ptr = NULL so the pointer honestly reflects that it points to nothing.
  • Mistake 3: Calculating realloc size in elements instead of bytes — Symptom: you write realloc(arr, new_capacity) when you meant realloc(arr, new_capacity * sizeof(int)), silently allocating four times fewer bytes than intended, causing writes past the allocation boundary that corrupt adjacent heap memory — Fix: always multiply element count by sizeof(element_type) in every malloc and realloc call — make it a habit you cannot skip.

Interview Questions on This Topic

  • QWhy does the doubling strategy for dynamic array growth give amortised O(1) insertion, and what would happen to the time complexity if you grew by a fixed number of slots (e.g., always add 10) instead?
  • QWhat is the correct way to use realloc so you don't leak memory if it fails? Write the pattern out.
  • QA colleague says 'dynamic arrays and linked lists both resize at runtime, so just pick whichever'. Where would you push back — and what specific workload characteristic would make you choose one over the other?

Frequently Asked Questions

What is the difference between malloc and calloc when creating a dynamic array in C?

malloc allocates a block of the requested size and leaves its contents uninitialised — you get whatever bytes happen to be sitting in that heap region. calloc allocates the same block but zeroes every byte before returning. For a dynamic array of integers, use calloc if you need a guaranteed starting value of zero; use malloc if you're going to overwrite every element anyway (e.g., reading from a file), since the zero-initialisation step is wasted work.

Can I use a Variable Length Array (VLA) in C instead of malloc for a runtime-sized array?

VLAs (int arr[n] where n is a variable) were added in C99 and let you put a runtime-sized array on the stack. They're gone from stack when the function returns, which is often not what you want. More importantly, they were made optional in C11 and are absent from many embedded toolchains, and a large VLA can silently overflow the stack with no error message. For production C code, malloc is the reliable, portable choice.

How do I know when my dynamic array should shrink, and by how much?

The standard heuristic is: shrink when count drops below one quarter of capacity, and shrink to half of capacity. This creates hysteresis — the array must lose three quarters of its entries before shrinking, so a single remove-then-add cycle doesn't trigger a pointless reallocate-grow loop. Never shrink below a sensible minimum capacity (such as 4 or 8 elements) to avoid thrashing on very small arrays.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousSTL Deque in C++Next →Coroutines in C++20
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged