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