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.
✦ Definition~90s read
What is Dynamic Arrays in C?
Dynamic arrays in C solve the fundamental tension between compile-time fixed sizes and runtime data unpredictability. Unlike stack-allocated arrays where you must know the maximum size at compile time — or waste memory by over-allocating — dynamic arrays grow and shrink on the heap using malloc, realloc, and free.
★
Imagine you're setting up chairs for a party but you don't know how many guests are coming.
They're the C equivalent of std::vector in C++ or ArrayList in Java, but without any language-level safety nets. You own every byte, every pointer, and every failure mode. Use them when you need to accumulate data incrementally (reading lines from a file, parsing network packets, building a list of results) and cannot bound the size in advance.
Don't use them for fixed-size, performance-critical hot paths where stack allocation avoids heap overhead entirely.
The core mechanism is the doubling strategy: when the array's capacity is exhausted, you realloc to a new block typically 1.5x or 2x the old size. This amortizes the O(n) copy cost to O(1) per insertion, giving you linear overall performance for appending n elements.
The growth factor matters deeply — 2x wastes more memory but reduces realloc frequency; 1.5x is more memory-efficient but triggers more copies. Production code often uses 1.5x or the golden ratio (~1.618) to balance fragmentation and throughput. Real-world implementations like stb_ds or GArray in GLib use exactly these patterns, and you'll find them in Redis, SQLite, and every serious C codebase that handles dynamic collections.
Beyond growth, production dynamic arrays must handle shrinking (to free memory when utilization drops), searching (typically linear or binary if sorted), and removal (shifting elements or marking as deleted). Memory fragmentation is the silent killer — frequent realloc calls can fragment the heap, especially with aggressive growth factors.
Tools like Valgrind, AddressSanitizer, and malloc_stats are essential for debugging leaks, buffer overruns, and double-frees. The trade-off is always between simplicity, performance, and memory overhead; a well-tuned dynamic array in C can match or beat higher-level languages for throughput, but requires meticulous attention to ownership, error handling, and realloc failure (which returns NULL and leaks the original block if you overwrite your pointer).
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.
Static arrays are fine until your data outgrows them. Dynamic arrays in C solve the exact problem of unknown size at compile time—giving you a growable buffer without the overhead of linked lists. Without them, you’re either hardcoding limits, wasting memory, or rewriting half your code when requirements change.
What Dynamic Arrays in C Actually Solve
A dynamic array in C is a contiguous block of memory that grows at runtime via realloc. Unlike fixed-size arrays, it decouples capacity from logical size: you track a length (elements used) and a capacity (memory allocated). The core mechanic is geometric growth — typically doubling capacity on each resize — to amortize O(1) append cost. Without this, every push would be O(n) due to full reallocation.
In practice, a dynamic array is a struct: a pointer to heap memory, a size_t for length, and a size_t for capacity. The critical property is that realloc may move the block, invalidating all existing pointers into the array. This is the single most common source of bugs in production C code. The pattern is: never hold a pointer to an element across a realloc call; always re-fetch the base address after any operation that could resize.
Use dynamic arrays when you need cache-friendly, contiguous storage with unpredictable size — reading lines from a file, building a list of network connections, or accumulating sensor data. They outperform linked lists for iteration and random access (O(1) vs O(n)) and use less memory per element. In real systems, they are the backbone of string builders, arena allocators, and serialization buffers.
Pointer Invalidation Trap
After realloc, all pointers into the old buffer are dangling. Never cache a pointer to an element across a resize — always recompute from the new base address.
Production Insight
A network server stores pointers into a dynamic array of connection structs. A single client triggers a resize, realloc moves the buffer, and all other pointers now point to freed memory — next read corrupts the heap.
Symptom: intermittent segfaults or silent data corruption on the next client request, only under load when the array grows.
Rule: never store pointers into a growable buffer; store indices (which remain valid after realloc) and dereference through the base pointer each time.
After any realloc, all pointers into the old buffer are dead — use indices or re-fetch.
Always check realloc return for NULL; a failure leaks the original block if you overwrote the pointer.
thecodeforge.io
Dynamic Arrays in C: Safe realloc Patterns
Dynamic Arrays C
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.
intmain(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]; // <-- VariableLengthArray, avoid in production
// HEAP approach — safe, portable, works at any size:
// malloc(n * sizeof(double)) asks the OSfor exactly n doubles worth of bytes
double *temperatures = malloc(item_count * sizeof(double));
// malloc returns NULLif 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);
return1; // 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 dothisfor you
free(temperatures);
temperatures = NULL; // null the pointer so it can't be accidentally used again
return0;
}
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
voidarray_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
voidarray_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
voidarray_print(constIntArray *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
voidarray_free(IntArray *arr) {
free(arr->data);
arr->data = NULL; // prevent use-after-free
arr->count = 0;
arr->capacity = 0;
}
intmain(void) {
IntArray scores;
array_init(&scores);
// Push10 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);
return0;
}
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;
voidarray_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); }
}
voidarray_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
intarray_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--;
// Shrinkif 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;
}
voidarray_print(constIntArray *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");
}
voidarray_free(IntArray *arr) {
free(arr->data);
arr->data = NULL;
arr->count = arr->capacity = 0;
}
intmain(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);
return0;
}
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
intmain(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);
return0;
}
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
intmain(void) {
int *arr = malloc(5 * sizeof(int)); // capacity for5 ints
if (!arr) return1;
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 ASanfree(arr);
return0;
}
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
IfWant to catch bugs before CI (during development)
→
UseUse clang-tidy or cppcheck static analysis in your editor.
malloc vs calloc: When Zero-Init Costs You Performance
You've seen malloc in every tutorial. Here's when not to use it.
malloc grabs a slab of heap and returns a pointer. The bytes are uninitialised — stale data from whatever freed that block last. If you're building a dynamic array that will be immediately overwritten by hot data (say, reading frames from a socket ring buffer), malloc wins. No pointless zero-fill cycles.
calloc does two things: allocates and zero-initialises every byte. Intuition says "free stuff." Reality says calloc can be slower because memset must touch every page. On a 100-million-element uint32_t array, that's 400 MB of writes before your first assignment. The trade-off: deterministic startup state. No garbage pointers, no uninitialised reads that corrupt production silently.
Senior rule: Use calloc when your structure has pointers that must start NULL. Use malloc when you're filling the buffer immediately with known data. Never use either without checking the return value — NULL means the OS said no.
calloc does not guarantee your memory is 'secure.' On first touch, the OS may zero-fill lazily via copy-on-write. If you need cryptographic zeroing, use explicit_bzero or memset_s after freeing.
Key Takeaway
malloc for speed, calloc for safety — but never default to one without measuring.
Flexible Array Members: The Zero-Overhead Struct Trick
Standard dynamic arrays need two allocations: one for the struct metadata, one for the data. Flexible array members (FAMs) collapse that into one. C99 introduced this, and most production codebases still ignore it.
The trick: declare a struct with fields, then a trailing array with no size. When allocating, malloc the struct size plus the array size. The array lives immediately after the fixed fields — one contiguous block, one free call.
Why this matters for real systems: Data locality. The metadata (length, capacity) and the data sit next to each other in cache. No pointer chasing through an indirection layer. Every access to arr->data[i] is a simple base-plus-offset, not two pointer dereferences.
Pitfall: The array must be the last member. You cannot have a FAM and then another field. Also, sizeof the struct returns the size ignoring the FAM — you track array length yourself.
Use FAMs for packet buffers, serialised messages, or any hot-path dynamic array where allocations dominate runtime.
When a struct with FAM is in a union, the FAM is not allowed (C11). Workaround: keep the FAM in its own struct and embed that struct in the union.
Key Takeaway
Flexible array members halve your allocation calls and keep hot data in one cache line.
Prerequisites: What You Must Understand Before Touching Dynamic Arrays
Dynamic arrays in C are not a beginner topic. You need a solid grasp of pointers, because every operation — resize, access, element removal — works through indirection. Understand pointer arithmetic and the difference between p[i] and (p + i). You must be comfortable with manual memory management: malloc, realloc, and free are your tools, and forgetting one free means a leak that accumulates silently. Heap allocation is slower than stack allocation; each malloc call involves an OS syscall or a bump into a free list. If you are writing real-time or embedded code, dynamic allocation is often banned outright. Finally, know your data types: sizeof is evaluated at compile time, and using the wrong type in realloc(sizeof(T) n) corrupts the heap. Without these foundations, dynamic arrays will crash your program in ways stack arrays never could.
Prerequisites.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — c-cpp tutorial// 25 lines max
#include <stdlib.h>
#include <stdio.h>
intmain() {
// You must know: sizeof is evaluated at compile time
size_t count = 10;
int *arr = (int*)malloc(count * sizeof(int));
if (!arr) return1;
arr[0] = 42; // pointer arithmetic: *(arr + 0)printf("%d\n", arr[0]);
free(arr); // forget this = memory leakreturn0;
}
Output
42
Production Trap:
Do not hide malloc behind a macro. Production C relies on explicit allocation — hiding it makes static analysis miss leaks.
Key Takeaway
Master pointers and sizeof before writing a single dynamic array.
Improvements — Struct Implementation: Encapsulating the Mess
Bare dynamic array code scatters size, capacity, and data across the scope. The struct implementation fixes this: bundle a pointer to the heap block, the logical count of elements, and the allocated capacity into one object. Every operation — da_append, da_remove, da_free — takes a pointer to this struct. This eliminates global variables and reduces function signatures from three parameters to one. The struct is small (typically three words on 64-bit), and passing it by pointer is cheap. A hidden improvement: you can now add a growth factor and a shrink threshold as struct fields, making the strategy configurable per array. Never expose the raw int*; expose typed access via da_get(da, i) that returns a pointer — then the caller can dereference or assign without knowing the internal layout. This is the minimal viable C abstraction: no vtables, no inheritance, just data + functions.
realloc can return NULL. Always assign to a temp pointer first, or you leak the original block on failure.
Key Takeaway
A struct bundles state — pass pointers, return void. This prevents scatter and leak bugs.
● 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.
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 / Aspect
Static Array (stack)
Dynamic Array (heap)
Size known at compile time?
Required — must be a constant
Not needed — set at runtime
Memory location
Stack — automatic cleanup
Heap — manual free required
Resize after creation
Impossible
Yes, via realloc
Access speed (indexing)
O(1) — identical
O(1) — identical
Insert at middle
Impossible after declaration
O(n) — requires shifting
Append to end (amortised)
N/A — fixed size
O(1) — with doubling strategy
Risk of memory leak
None — stack auto-cleans
Yes — must call free
Risk of stack overflow
Yes — large arrays overflow stack
No — heap is much larger
Cache friendliness
Excellent — contiguous
Excellent — contiguous
Lifetime
Until function returns
Until free is called
Fragmentation risk
None
Yes — from repeated realloc of different sizes
Tools for debugging
None 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.
Q02 of 05SENIOR
What is the correct way to use realloc so you don't leak memory if it fails? Write the pattern out.
ANSWER
Use a temporary pointer: int tmp = realloc(arr, new_size sizeof(int)); if (tmp == NULL) { / handle error — arr still points to old data, so you can continue or recover / return; } arr = tmp;. This ensures that if realloc fails, the original pointer remains valid and you don't lose access to the old block.
Q03 of 05SENIOR
A 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?
ANSWER
Dynamic arrays give O(1) random access and excellent cache locality but suffer O(n) insertion/removal in the middle. Linked lists give O(1) insertion/removal given a pointer, but O(n) random access and terrible cache performance due to pointer-chasing. Choose dynamic array when workloads are dominated by iteration, random access, or appends/removals at the end. Choose linked list when you frequently insert or remove in the middle with many elements, and you don't need random access.
Q04 of 05SENIOR
Explain how to implement a dynamic array that supports generic element types in C (i.e., works with any data type).
ANSWER
Use void as the element type or define a macro-based generic structure. For a void approach, store the element size: typedef struct { void data; int count; int capacity; size_t elem_size; } DynArray. For element access via memcpy, you lose type safety. For macro-based, use X-Macros or #define DYNARRAY(type, name) ... to generate a type-specific struct and functions. The macro approach is more common in embedded systems, while void is simpler for prototyping.
Q05 of 05JUNIOR
What is the difference between malloc and calloc when creating a dynamic array?
ANSWER
malloc allocates uninitialized memory — its contents are whatever was previously in that heap region. calloc allocates and zero-initializes every byte. For dynamic arrays, use calloc if zero initialization is required (e.g., arrays of counters). But calloc is slower due to the zeroing, so if you plan to immediately overwrite every element (e.g., reading from a file), use malloc and skip the unnecessary write.
01
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?
SENIOR
02
What is the correct way to use realloc so you don't leak memory if it fails? Write the pattern out.
SENIOR
03
A 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?
SENIOR
04
Explain how to implement a dynamic array that supports generic element types in C (i.e., works with any data type).
SENIOR
05
What is the difference between malloc and calloc when creating a dynamic array?
JUNIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
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.
Was this helpful?
02
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.
Was this helpful?
03
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.
Was this helpful?
04
What is memory fragmentation and how does it affect dynamic arrays?
Fragmentation occurs when allocated blocks are not adjacent, leaving unusable gaps in the heap. Repeated realloc of varying sizes creates holes that are too small for new allocations. This can cause out-of-memory errors even when total free memory is high. Mitigate by using a lower growth factor (1.5 instead of 2) or pre-allocating a large buffer upfront.