Junior 6 min · March 06, 2026

Dynamic Arrays in C — Safe realloc Patterns for Production

A realloc failure leaked 200K sensor readings and crashed a server.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Dynamic arrays allocate heap memory at runtime via malloc, grow with realloc, and release with free.
  • Capacity doubling gives amortized O(1) append — growing by fixed increments causes O(n²) copying.
  • Always realloc into a temporary pointer: direct assignment leaks memory if realloc fails.
  • Shrink when count falls below capacity/4, shrink to capacity/2 — prevents thrashing on remove-then-add cycles.
  • Memory fragmentation rises with many small reallocations; use power-of-two sizes to mitigate.
Plain-English First

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.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#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 Check
malloc 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.
Production Insight
Stack overflow from a large VLA can crash your program silently with no stack trace.
Heap allocation via malloc gives you a clear NULL return on failure — that's better.
Rule: For runtime-sized arrays, always go heap. Never use VLAs in production code.
Key Takeaway
Heap allocation exists because stack arrays can't handle runtime sizes.
malloc gives you control — but with control comes the responsibility to free.
Null check after malloc is not optional; it's your first line of defense against crashes.
When to choose heap over stack for arrays
IfArray size known at compile time AND < 4KB
UseUse stack array — faster allocation, no free needed.
IfArray size determined at runtime or > 4KB
UseUse heap with malloc — portable, safe, avoids stack overflow.
IfArray must survive function return
UseHeap is the only choice — stack memory vanishes when the function exits.

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.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#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 Optional
Writing 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.
Production Insight
Doubling works brilliantly for most cases, but on memory-constrained systems it can cause sudden large allocations.
If you're running on an embedded device, consider a growth factor of 1.5 or use a pool allocator.
Rule: The temporary pointer pattern isn't just good practice — it's your safety net against silent data loss.
Key Takeaway
Doubling capacity gives amortized O(1) append — the same strategy used by std::vector and ArrayList.
Growing by a fixed number of slots makes each insert O(n) in the worst case.
The temporary pointer pattern for realloc is the single most important defense against memory leaks in C.
Choose your growth factor
IfMaximum throughput is the goal, memory is abundant
UseUse doubling (growth factor 2). Simple and amortized O(1).
IfLong-running server with many containers, memory fragmentation a concern
UseUse a lower growth factor (1.5 or golden ratio ~1.618) to reduce fragmentation.
IfReal-time system where allocation latency must be bounded
UsePre-allocate capacity upfront based on worst-case estimate — avoid realloc in hot paths.

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.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#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.
Production Insight
Shrinking on every remove causes disastrous reallocation thrashing — always use a threshold.
The hysteresis pattern (shrink only when count < capacity/4) is battle-tested across std::vector, Java ArrayList, and Python list.
Rule: For removal-heavy workloads at arbitrary positions, consider a linked list or a rope data structure instead of a dynamic array.
Key Takeaway
Shrink with hysteresis (quarter threshold) to avoid thrashing, and use memmove, not memcpy, for overlapping shifts.
Removal from the middle costs O(n) — know your workload before choosing a data structure.
The hysteresis rule: shrink to half when count drops below a quarter — this bounds wasted space without penalizing transient removals.
Dynamic array vs linked list for removals
IfRemovals are always from the end (stack-like behavior)
UseDynamic array is best — O(1) removal, excellent cache locality.
IfRemovals are rare and scattered
UseDynamic array still wins — the O(n) shift cost is acceptable, and you keep O(1) random access.
IfFrequent random removals in the middle
UseUse a linked list or a balanced tree — O(n) shift per removal becomes too expensive.

Memory Fragmentation and Choosing the Right Growth Factor

You've mastered the doubling strategy, but in long-running production systems, doubling can silently create a new problem: memory fragmentation. Each time realloc runs, the operating system may place the new block at a different address, leaving a free hole behind. Over time, these holes fill the heap with unusable gaps — a condition known as external fragmentation. Your process might technically have enough free bytes, but no contiguous block large enough to satisfy the next allocation.

Doubling from a small initial size (say 4) to huge numbers (512, 1024, 2048) exacerbates fragmentation because each growing block is a different size, making it hard for the allocator to reuse free holes. The fix is to use a lower growth factor — 1.5 (or the golden ratio 1.618) is common — which generates more repeatable block sizes and gives the allocator a better chance at reusing freed memory. Many high-performance memory allocators (jemalloc, tcmalloc) already use size classes that align with such factors.

Another approach is to pre-allocate a large enough buffer upfront. If you can bound the maximum size of your dynamic array, allocate that full capacity at init time and avoid realloc entirely. This eliminates fragmentation at the cost of raw memory usage — a trade-off worth considering for real-time systems or embedded devices.

growth_factor_comparison.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>

// Compare memory behaviour of doubling vs golden ratio growth
// Run with: gcc -o growth growth_factor_comparison.c && ./growth

int main(void) {
    int capacity_double = 4;
    int capacity_golden = 4;
    int count = 100;

    printf("Capacity progression for %d insertions:\n", count);
    printf("Insert  Doubling  Golden(1.618)\n");

    for (int i = 0; i < count; i++) {
        printf("%6d  %8d  %8d\n", i+1, capacity_double, capacity_golden);

        if (i == capacity_double) capacity_double *= 2;
        if (i == capacity_golden) capacity_golden = (int)(capacity_golden * 1.618) + 1;
    }

    printf("\nFinal capacity using doubling: %d\n", capacity_double);
    printf("Final capacity using golden:    %d\n", capacity_golden);
    return 0;
}
Output
Insert Doubling Golden(1.618)
1 4 4
2 4 4
3 4 4
4 4 4
5 8 7
6 8 7
7 8 12
8 8 12
9 16 12
...
Final capacity using doubling: 128
Final capacity using golden: 118
Pro Tip: Define the growth factor as a macro for easy tuning
#define GROWTH_FACTOR 1.618 // golden ratio // In your push function: if (arr->count == arr->capacity) { int new_cap = (int)(arr->capacity * GROWTH_FACTOR) + 1; // ...realloc... } This lets you experiment with different factors without changing logic.
Production Insight
Fragmentation from repeated realloc can cause OOM errors even when 'enough' memory is free.
Tools like valgrind --tool=massif can show you actual heap usage vs allocated chunks.
Rule: For long-lived containers in server processes, use a growth factor of 1.5–1.618 instead of 2 to keep block sizes more uniform and reduce fragmentation.
Key Takeaway
Doubling is fast but fragments the heap over time in long-running systems.
A growth factor of 1.5 significantly reduces fragmentation at a small cost in total allocations.
If you know the maximum size, pre-allocate — no realloc means no fragmentation.
Growth factor selection guide
IfArray lifetime is short (seconds/minutes) or total size is small
UseUse doubling (factor 2). Simple, fast, fragmentation is negligible.
IfArray lives for hours/days in a server process with multiple allocations
UseUse a factor of 1.5 or golden ratio to reduce fragmentation.
IfMaximum size is known and bounded
UsePre-allocate the full capacity upfront. Eliminates realloc entirely.
IfReal-time constraints with deterministic latency requirements
UsePre-allocate or use a fixed-size ring buffer — avoid realloc in the hot path.

Debugging Dynamic Arrays – Tools and Techniques

Even with correct code, dynamic arrays can hide bugs that only surface after hours of production runtime. Buffer overflows, use-after-free, and off-by-one errors are the most common. The good news: modern tools catch them before they reach production if you run them in your test suite.

AddressSanitizer (ASan) is the fastest way to detect buffer overflows, use-after-free, and out-of-bounds accesses. Compile your code with -fsanitize=address and you get instant, detailed error reports on every violation. It's memory-efficient and integrates with valgrind for a second layer. Valgrind's memcheck tool is slower but catches some things ASan can't, like uninitialized memory reads.

Static analysis (like clang-tidy or PVS-Studio) can catch common patterns like direct realloc overwrite before runtime. But they can't catch everything. A good strategy: run ASan in unit tests, valgrind in integration tests, and use a memory profiler (valgrind massif) in long-running stress tests to detect fragmentation.

debug_demo.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>

// Intentionally create a buffer overflow to show how AddressSanitizer catches it
// Compile: gcc -g -fsanitize=address -o debug_demo debug_demo.c && ./debug_demo

int main(void) {
    int *arr = malloc(5 * sizeof(int));  // capacity for 5 ints
    if (!arr) return 1;

    for (int i = 0; i <= 5; i++) {  // Off-by-one: writes to arr[5] which is out of bounds
        arr[i] = i * 10;
    }

    printf("arr[5] = %d\n", arr[5]);  // This line will never be reached with ASan
    free(arr);
    return 0;
}
Output
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000014
WRITE of size 4 at 0x602000000014 thread T0
#0 0x4007a1 in main debug_demo.c:10
0x602000000014 is located 4 bytes after 20-byte region [0x602000000000,0x602000000014)
allocated by:
#0 0x4005b0 in malloc (/debug_demo+0x4005b0)
#1 0x400784 in main debug_demo.c:7
ASan vs Valgrind: When to Use Which
AddressSanitizer is fast (<2x slowdown) and catches overflows/use-after-free instantly. Valgrind (memcheck) is slower (10-20x) but catches uninitialized reads and has a more complete memory model. Use ASan in unit tests, Valgrind in integration and stress tests.
Production Insight
A production bug: off-by-one in array indexing caused silent heap corruption that only manifested as a crash in a completely unrelated part of the system 30 minutes later.
ASan would have caught it on the first test run.
Rule: Add ASan to your CI pipeline. It catches bugs at the moment of corruption, not when the symptoms finally appear.
Key Takeaway
AddressSanitizer catches heap bugs instantly — compile with -fsanitize=address in development.
Valgrind is slower but provides more detailed memory analysis for leaks and uninitialized reads.
Never rely on manual inspection: automated tools are the only way to catch subtle memory bugs in C.
Which debugging tool for which symptom
IfSuspected buffer overflow or use-after-free
UseRun with AddressSanitizer (compile with -fsanitize=address).
IfSuspected memory leak or uninitialised read
UseRun with Valgrind: valgrind --tool=memcheck ./program
IfNeed to visualise heap fragmentation over time
UseUse Valgrind's massif tool: valgrind --tool=massif ./program
IfWant to catch bugs before CI (during development)
UseUse clang-tidy or cppcheck static analysis in your editor.
● Production incidentPOST-MORTEMseverity: high

The Lost Sensor Readings – realloc Failure That Silently Corrupted a Server

Symptom
After running for 12 hours, the telemetry server started returning empty results for recent sensor data. Memory usage had dropped but the process eventually crashed with a segfault. Logs showed no errors.
Assumption
The engineer assumed realloc always succeeds because 'servers have enough memory'. They used the direct assignment pattern: arr = realloc(arr, new_size);
Root cause
During a spike in sensor readings, realloc tried to allocate a very large block (500K ints). The system was near memory pressure, so realloc returned NULL. The original pointer was overwritten in arr, making the ~200K already-collected readings unreachable and unfreeable. Subsequent attempts to write to the NULL pointer caused the segfault.
Fix
Changed all realloc calls to use a temporary pointer: int tmp = realloc(arr->data, new_sz); if (!tmp) { / handle error */ } arr->data = tmp;. And added a growth cap to avoid requesting excessively large blocks in one go.
Key lesson
  • Never overwrite the original pointer with the result of realloc without checking for NULL first.
  • Memory allocation can fail even on servers — always handle the failure gracefully.
  • For production systems, cap growth to a reasonable maximum to avoid sudden huge allocations.
Production debug guideSymptom → Action guide for the three most common dynamic array bugs in production3 entries
Symptom · 01
Segfault when accessing array elements after a realloc
Fix
Check the realloc pattern: are you using a temporary pointer? If not, a failed realloc overwrites your pointer with NULL. Add error handling for realloc failure.
Symptom · 02
Memory usage grows without bound, system runs out of memory
Fix
Check for missing free calls. Run valgrind --tool=memcheck ./program to find leaked blocks. Also verify that every malloc/calloc is matched with a free on all code paths.
Symptom · 03
Random data corruption after many push/pop operations
Fix
Check for off-by-one errors in indexing. Are you writing beyond the allocated capacity? Use AddressSanitizer (compile with -fsanitize=address) to catch buffer overflows precisely.
★ Dynamic Array Debugging Cheat SheetQuick commands and fixes for the most frequent production issues with dynamic arrays in C.
Memory leak (allocated memory never freed)
Immediate action
Identify leaked allocations with Valgrind
Commands
valgrind --leak-check=full --show-leak-kinds=all ./myprogram 2>&1 | grep 'definitely lost'
valgrind --tool=memcheck --track-origins=yes ./myprogram
Fix now
Add a free() call for every malloc/realloc return. Use -fsanitize=address which logs unfreed allocations at exit.
Heap buffer overflow (write past allocated limits)+
Immediate action
Compile with AddressSanitizer and run
Commands
gcc -g -fsanitize=address -o myprogram myprogram.c
./myprogram (will abort with a detailed error showing the exact line of overflow)
Fix now
Check loop bounds: ensure you never access arr[count] when count is the capacity. Use arr->data[index] only if index < arr->count.
Use-after-free (touching memory after it's freed)+
Immediate action
Run with AddressSanitizer to catch it immediately
Commands
gcc -g -fsanitize=address -o myprogram myprogram.c && ./myprogram
valgrind --tool=memcheck --free-fill=0xAA ./myprogram -- marks freed memory with a pattern
Fix now
Set pointer to NULL after free: free(ptr); ptr = NULL; so any subsequent usage crashes intentionally instead of corrupting data.
Invalid free (freeing non-heap memory or double free)+
Immediate action
Run Valgrind to identify the invalid free site
Commands
valgrind --tool=memcheck ./myprogram 2>&1 | grep 'Invalid free'
gdb ./myprogram core (if core dump generated, inspect the call stack)
Fix now
Ensure you only free pointers returned by malloc/calloc/realloc. Avoid freeing stack variables. Set freed pointers to NULL to prevent double free.
Static Arrays vs Dynamic Arrays in C
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
Fragmentation riskNoneYes — from repeated realloc of different sizes
Tools for debuggingNone needed (stack cleans itself)Valgrind, ASan, massif required for robustness

Key takeaways

1
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.
2
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.
3
realloc must go into a temporary pointer first
overwriting your original pointer directly causes an unrecoverable memory leak if the allocation fails.
4
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.
5
Use a shrink threshold (count < capacity/4) to avoid reallocation thrashing when items are removed and re-added.
6
For long-running servers, consider a growth factor lower than 2 (e.g., 1.5) to reduce heap fragmentation.
7
Compile with -fsanitize=address during development; it catches buffer overflows and use-after-free that Valgrind might miss in optimized builds.

Common mistakes to avoid

5 patterns
×

Direct realloc assignment overwriting original pointer

Symptom
If realloc returns NULL (out of memory), the only pointer to the old block is lost, causing a permanent memory leak. Subsequent code dereferences NULL and crashes.
Fix
Always realloc into a temporary pointer, check for NULL, then assign: int tmp = realloc(arr, new_sz); if (!tmp) { / handle error */ return; } arr = tmp;
×

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.
×

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.
×

Shrinking the array on every removal (no threshold)

Symptom
Remove one element → shrink to exact count. Then add one element → grow back. This causes repeated realloc calls, thrashing performance and fragmenting the heap.
Fix
Use a hysteresis threshold: shrink only when count < capacity/4, and shrink to capacity/2. This prevents thrashing on transient removals.
×

Using memcpy for overlapping memory shift when removing an element

Symptom
memcpy is undefined behaviour when source and destination overlap. When you shift elements left in the same array, they always overlap. The result is silent data corruption that varies by platform and compiler.
Fix
Always use memmove for overlapping copies. memmove handles the direction internally and is guaranteed safe.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
Why does the doubling strategy for dynamic array growth give amortised O...
Q02SENIOR
What is the correct way to use realloc so you don't leak memory if it fa...
Q03SENIOR
A colleague says 'dynamic arrays and linked lists both resize at runtime...
Q04SENIOR
Explain how to implement a dynamic array that supports generic element t...
Q05JUNIOR
What is the difference between malloc and calloc when creating a dynamic...
Q01 of 05SENIOR

Why 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?

ANSWER
Doubling ensures that the total number of element copies across all insertions is O(n) — each element is copied at most log2(n) times. With a fixed increment of 10, each insertion triggers a linear copy of all current elements, leading to O(n²) total work for n insertions. The amortized cost drops from O(1) per insertion to O(n). This is why all major languages use a multiplicative growth factor.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between malloc and calloc when creating a dynamic array in C?
02
Can I use a Variable Length Array (VLA) in C instead of malloc for a runtime-sized array?
03
How do I know when my dynamic array should shrink, and by how much?
04
What is memory fragmentation and how does it affect dynamic arrays?
🔥

That's C Basics. Mark it forged?

6 min read · try the examples if you haven't

Previous
Bitwise Operators in C
15 / 17 · C Basics
Next
typedef and enum in C