Dynamic Arrays in C — Safe realloc Patterns for Production
A realloc failure leaked 200K sensor readings and crashed a server.
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
- 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.
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.
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).
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.
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.
#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; }
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.
#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; }
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.
#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;
}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.
#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; }
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.
#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; }
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.
// io.thecodeforge — c-cpp tutorial #include <stdlib.h> #include <stdio.h> #include <time.h> int main(void) { const size_t count = 100000000; // 100 million ints clock_t start, end; // malloc — no init start = clock(); int* raw = (int*)malloc(count * sizeof(int)); end = clock(); printf("malloc: %ld ticks\n", end - start); // calloc — zero-init start = clock(); int* zeroed = (int*)calloc(count, sizeof(int)); end = clock(); printf("calloc: %ld ticks\n", end - start); free(raw); free(zeroed); return 0; }
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.
// io.thecodeforge — c-cpp tutorial #include <stdlib.h> #include <stdio.h> #include <string.h> typedef struct { size_t length; unsigned char data[]; // flexible array member — no size } Buffer; Buffer* buffer_create(size_t payload_size) { Buffer* buf = (Buffer*)malloc(sizeof(Buffer) + payload_size); if (!buf) return NULL; buf->length = payload_size; return buf; } int main(void) { Buffer* msg = buffer_create(256); memcpy(msg->data, "Hello from single-heap world\n", 28); printf("%s", msg->data); printf("Struct size: %zu bytes\n", sizeof(Buffer)); free(msg); // one free, no leak return 0; }
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.
// io.thecodeforge — c-cpp tutorial // 25 lines max #include <stdlib.h> #include <stdio.h> int main() { // You must know: sizeof is evaluated at compile time size_t count = 10; int *arr = (int*)malloc(count * sizeof(int)); if (!arr) return 1; arr[0] = 42; // pointer arithmetic: *(arr + 0) printf("%d\n", arr[0]); free(arr); // forget this = memory leak return 0; }
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.
// io.thecodeforge — c-cpp tutorial // 25 lines max #include <stdlib.h> typedef struct { int *data; size_t size; size_t capacity; } DynArray; void da_append(DynArray *da, int value) { if (da->size >= da->capacity) { da->capacity = da->capacity ? da->capacity * 2 : 4; da->data = (int*)realloc(da->data, da->capacity * sizeof(int)); } da->data[da->size++] = value; } void da_free(DynArray *da) { free(da->data); da->data = NULL; da->size = da->capacity = 0; }
The Lost Sensor Readings – realloc Failure That Silently Corrupted a Server
- 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.
valgrind --leak-check=full --show-leak-kinds=all ./myprogram 2>&1 | grep 'definitely lost'valgrind --tool=memcheck --track-origins=yes ./myprogramfree() call for every malloc/realloc return. Use -fsanitize=address which logs unfreed allocations at exit.gcc -g -fsanitize=address -o myprogram myprogram.c./myprogram (will abort with a detailed error showing the exact line of overflow)gcc -g -fsanitize=address -o myprogram myprogram.c && ./myprogramvalgrind --tool=memcheck --free-fill=0xAA ./myprogram -- marks freed memory with a patternvalgrind --tool=memcheck ./myprogram 2>&1 | grep 'Invalid free'gdb ./myprogram core (if core dump generated, inspect the call stack)| 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 |
| File | Command / Code | Purpose |
|---|---|---|
| heap_vs_stack.c | int main(void) { | Why malloc? Understanding Heap Allocation vs Stack Arrays |
| dynamic_array_grow.c | typedef struct { | Growing a Dynamic Array with realloc |
| dynamic_array_remove.c | typedef struct { | Shrinking, Searching and Removing |
| growth_factor_comparison.c | int main(void) { | Memory Fragmentation and Choosing the Right Growth Factor |
| debug_demo.c | int main(void) { | Debugging Dynamic Arrays – Tools and Techniques |
| AllocShowdown.c | int main(void) { | malloc vs calloc |
| FlexibleArray.c | typedef struct { | Flexible Array Members |
| Prerequisites.cpp | int main() { | Prerequisites |
| StructImpl.cpp | typedef struct { | Improvements |
Key takeaways
Common mistakes to avoid
5 patternsDirect realloc assignment overwriting original pointer
Not nullifying a pointer after calling free
Calculating realloc size in elements instead of bytes
Shrinking the array on every removal (no threshold)
Using memcpy for overlapping memory shift when removing an element
Interview Questions on This Topic
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?
What is the correct way to use realloc so you don't leak memory if it fails? Write the pattern out.
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?
Explain how to implement a dynamic array that supports generic element types in C (i.e., works with any data type).
What is the difference between malloc and calloc when creating a dynamic array?
Frequently Asked Questions
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.
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.
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.
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.
20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.
That's C Basics. Mark it forged?
7 min read · try the examples if you haven't