Complete guide to dynamic arrays in C — creating arrays at runtime, growing arrays with realloc, implementing ArrayList-like behavior, 2D dynamic arrays, and practical examples with output.
In C, regular arrays have a fixed size determined at compile time. But real-world programs often need arrays that can grow or shrink based on user input, file data, or runtime conditions. Dynamic arrays solve this problem by combining malloc(), realloc(), and free() to create flexible, resizable containers.
The Problem with Static Arrays
#include <stdio.h>
int main() {
int arr[5]; // Fixed: exactly 5 elements, decided at compile time
int n;
printf("How many elements? ");
scanf("%d", &n);
// If n > 5, we overflow. If n < 5, we waste memory.
// Static arrays can't adapt!
return 0;
}
Creating a Dynamic Array
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("How many numbers do you want to store? ");
scanf("%d", &n);
// Allocate exactly n elements at runtime
int *arr = (int *)malloc(n * sizeof(int));
if (arr == NULL) {
printf("Allocation failed!\n");
return 1;
}
// Input values
printf("Enter %d numbers:\n", n);
for (int i = 0; i < n; i++) {
scanf("%d", &arr[i]);
}
// Process — find the sum
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
printf("Sum = %d, Average = %.2f\n", sum, (float)sum / n);
free(arr);
return 0;
}
How many numbers do you want to store? 4
Enter 4 numbers:
10 20 30 40
Sum = 100, Average = 25.00
Growing an Array (ArrayList Pattern)
The classic pattern doubles the capacity when the array is full:
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data;
int size; // Current number of elements
int capacity; // Total allocated slots
} DynamicArray;
DynamicArray *create_array(int initial_capacity) {
DynamicArray *arr = (DynamicArray *)malloc(sizeof(DynamicArray));
if (arr == NULL) return NULL;
arr->data = (int *)malloc(initial_capacity * sizeof(int));
if (arr->data == NULL) {
free(arr);
return NULL;
}
arr->size = 0;
arr->capacity = initial_capacity;
return arr;
}
int push_back(DynamicArray *arr, int value) {
if (arr->size == arr->capacity) {
// Double the capacity
int new_cap = arr->capacity * 2;
int *temp = (int *)realloc(arr->data, new_cap * sizeof(int));
if (temp == NULL) return -1; // Failure
arr->data = temp;
arr->capacity = new_cap;
printf(" [Grew capacity: %d -> %d]\n", arr->capacity / 2, new_cap);
}
arr->data[arr->size++] = value;
return 0;
}
void print_array(DynamicArray *arr) {
printf("[ ");
for (int i = 0; i < arr->size; i++) {
printf("%d ", arr->data[i]);
}
printf("] (size=%d, capacity=%d)\n", arr->size, arr->capacity);
}
void destroy_array(DynamicArray *arr) {
if (arr == NULL) return;
free(arr->data);
free(arr);
}
int main() {
DynamicArray *arr = create_array(2); // Start with capacity 2
if (arr == NULL) return 1;
// Push 8 elements — watch it grow
for (int i = 1; i <= 8; i++) {
printf("Pushing %d...\n", i * 10);
push_back(arr, i * 10);
}
printf("\nFinal array: ");
print_array(arr);
destroy_array(arr);
return 0;
}
Pushing 10...
Pushing 20...
Pushing 30...
[Grew capacity: 2 -> 4]
Pushing 40...
Pushing 50...
[Grew capacity: 4 -> 8]
Pushing 60...
Pushing 70...
Pushing 80...
Final array: [ 10 20 30 40 50 60 70 80 ] (size=8, capacity=8)
Why Double the Capacity?
| Growth Strategy | Amortized Cost per Push | Memory Overhead |
|---|
| +1 each time | O(n) — copies every time | Minimal |
| +10 each time | O(n) — still linear | Low |
| ×2 (double) | O(1) amortized | Up to 50% wasted |
| ×1.5 | O(1) amortized | Up to 33% wasted |
Doubling gives O(1) amortized insert time. Each element is copied at most O(log n) times total.
Dynamic 2D Array
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows, cols;
printf("Enter rows and cols: ");
scanf("%d %d", &rows, &cols);
// Allocate array of row pointers
int **matrix = (int **)malloc(rows * sizeof(int *));
if (matrix == NULL) return 1;
// Allocate each row
for (int i = 0; i < rows; i++) {
matrix[i] = (int *)malloc(cols * sizeof(int));
if (matrix[i] == NULL) {
// Cleanup on failure
for (int j = 0; j < i; j++) free(matrix[j]);
free(matrix);
return 1;
}
}
// Fill with multiplication table
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j] = (i + 1) * (j + 1);
}
}
// Print
printf("\nMultiplication table (%dx%d):\n", rows, cols);
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%4d", matrix[i][j]);
}
printf("\n");
}
// Free (rows first, then array of pointers)
for (int i = 0; i < rows; i++) free(matrix[i]);
free(matrix);
return 0;
}
Enter rows and cols: 4 5
Multiplication table (4x5):
1 2 3 4 5
2 4 6 8 10
3 6 9 12 15
4 8 12 16 20
Contiguous 2D Array (Single Allocation)
The pointer-to-pointer approach above has fragmented memory. For better cache performance, allocate one contiguous block:
#include <stdio.h>
#include <stdlib.h>
int main() {
int rows = 3, cols = 4;
// Single contiguous allocation
int *matrix = (int *)malloc(rows * cols * sizeof(int));
if (matrix == NULL) return 1;
// Access with: matrix[i * cols + j]
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i * cols + j] = i * 10 + j;
}
}
printf("Contiguous 2D array:\n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%3d ", matrix[i * cols + j]);
}
printf("\n");
}
free(matrix); // Single free!
return 0;
}
Contiguous 2D array:
0 1 2 3
10 11 12 13
20 21 22 23
Removing Elements from a Dynamic Array
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void remove_at(int *arr, int *size, int index) {
if (index < 0 || index >= *size) return;
// Shift elements left
memmove(&arr[index], &arr[index + 1], (*size - index - 1) * sizeof(int));
(*size)--;
}
int main() {
int capacity = 10;
int size = 6;
int *arr = (int *)malloc(capacity * sizeof(int));
arr[0] = 10; arr[1] = 20; arr[2] = 30;
arr[3] = 40; arr[4] = 50; arr[5] = 60;
printf("Before remove: ");
for (int i = 0; i < size; i++) printf("%d ", arr[i]);
printf("\n");
remove_at(arr, &size, 2); // Remove element at index 2 (value 30)
printf("After remove: ");
for (int i = 0; i < size; i++) printf("%d ", arr[i]);
printf("\n");
free(arr);
return 0;
}
Before remove: 10 20 30 40 50 60
After remove: 10 20 40 50 60
Memory Layout Comparison
| Static Array | int arr[5] |
| Dynamic Array | int *arr = malloc(5 * sizeof(int)) |
| │ ptr ─────┼───── | │ [0] [1] [2] [3] [4] │ |
| Dynamic 2D | int **matrix |
| │ matrix ──┼───── | │ [0] ────┼─────→│ cols... │ |
| └──────────┘ └─────────┘│ └── | │ cols... │ |
| └──── | │ cols... │ |
Interview Questions
Q1: What is the time complexity of inserting at the end of a dynamic array?
Amortized O(1) if you use a doubling strategy. Individual insertions that trigger a resize are O(n) (due to copying), but these happen rarely enough that the average cost per operation is constant.
Q2: How do you implement a dynamic array that shrinks?
When the size drops below 1/4 of the capacity, shrink to half capacity using realloc(). This prevents thrashing (repeated grow/shrink) at the boundary. Never shrink below a minimum capacity (e.g., 4).
Q3: What's the difference between VLA (Variable Length Arrays) and dynamic arrays?
VLAs (C99) are allocated on the stack with automatic lifetime — they can't be resized and can cause stack overflow for large sizes. Dynamic arrays are on the heap, can be resized with realloc(), and persist until explicitly freed.
Q4: Why is the contiguous 2D array approach better for performance?
Contiguous memory has better cache locality — the CPU can prefetch adjacent elements. The pointer-to-pointer approach scatters rows across the heap, causing cache misses. Contiguous arrays also require only one malloc/free call.
Summary
- Dynamic arrays use
malloc() to allocate arrays whose size is determined at runtime - The doubling strategy (grow by 2×) gives O(1) amortized insertion time
- Use a struct to encapsulate
data, size, and capacity - For 2D arrays, prefer contiguous allocation for performance-critical code
- Always free all allocated memory and handle realloc failures safely
- Dynamic arrays form the basis of many data structures (stacks, queues, hash tables)