Lab: Dynamic array (growable buffer)
Implement a growable dynamic array in C using malloc and realloc — the foundation of every dynamic collection in systems programming.
A dynamic array (also called a vector or growable buffer) is one of the most important data structures in systems programming. It stores elements in a contiguous block of memory (like a fixed array) but can grow as you add elements. This is how Python's list, C++'s std::vector, and Go's slices work internally.
Goal
Implement a DynArray that:
- Starts with capacity for 4 elements.
- Doubles its capacity with
reallocwhen full. - Supports
push(append),get(index access),len(current count), andfree_array(cleanup).
The struct
typedef struct {
int *data; /* heap-allocated array */
int len; /* number of elements currently stored */
int capacity; /* allocated capacity */
} DynArray;The API
void da_init(DynArray *da);
int da_push(DynArray *da, int value); /* returns 0 on success, -1 on failure */
int da_get(const DynArray *da, int index);
int da_len(const DynArray *da);
void da_free(DynArray *da);Implementation hints
da_init: Set data = NULL, len = 0, capacity = 0. Lazy allocation (allocate on first push) keeps init simple.
da_push: If len == capacity, grow. The growth strategy: if capacity == 0, set to 4; otherwise double. Use realloc with a temporary pointer — never assign directly to da->data.
da_get: Bounds-check the index and return da->data[index].
da_free: Call free(da->data) and reset fields to 0/NULL.
Worked solution
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data;
int len;
int capacity;
} DynArray;
void da_init(DynArray *da) {
da->data = NULL;
da->len = 0;
da->capacity = 0;
}
int da_push(DynArray *da, int value) {
if (da->len == da->capacity) {
int new_cap = (da->capacity == 0) ? 4 : da->capacity * 2;
int *tmp = realloc(da->data, new_cap * sizeof(int));
if (tmp == NULL) { return -1; }
da->data = tmp;
da->capacity = new_cap;
}
da->data[da->len++] = value;
return 0;
}
int da_get(const DynArray *da, int index) {
if (index < 0 || index >= da->len) {
fprintf(stderr, "Index %d out of bounds (len=%d)\n", index, da->len);
exit(1);
}
return da->data[index];
}
int da_len(const DynArray *da) {
return da->len;
}
void da_free(DynArray *da) {
free(da->data);
da->data = NULL;
da->len = 0;
da->capacity = 0;
}
int main(void) {
DynArray arr;
da_init(&arr);
for (int i = 0; i < 20; i++) {
if (da_push(&arr, i * i) != 0) {
fprintf(stderr, "push failed\n");
da_free(&arr);
return 1;
}
}
printf("Length: %d\n", da_len(&arr));
for (int i = 0; i < da_len(&arr); i++) {
printf("%d ", da_get(&arr, i));
}
printf("\n");
da_free(&arr);
return 0;
}Verify with valgrind
gcc -g -O0 dynarray.c -o dynarray
valgrind --leak-check=full ./dynarrayExpected: zero errors, zero leaks.
Extension challenges
- Add a
da_removefunction that removes an element by index and shifts the remaining elements left. - Store strings. Change
int *datatochar **dataand makeda_pushtake aconst char *, duplicating the string withstrdup(or your ownmy_strdup).da_freemust thenfreeeach string before freeing the array. - Shrink policy. When
len < capacity / 4,reallocdown tocapacity / 2. This prevents unbounded memory use after a large array is trimmed.
What you practised
- Dynamic allocation with
mallocandrealloc - The safe realloc pattern (temp pointer, NULL check, then assign)
- Struct-based data structures with a consistent API
- The doubling growth strategy and why it gives amortized O(1) push
The next module is Structs and enums — grouping related data into custom types, which is the foundation of every data structure you will build in C.
Valgrind basics
Use valgrind memcheck to detect memory errors — invalid reads/writes, use-after-free, memory leaks, and uninitialised value use.
Defining and using structs
Group related data into custom types with C structs — declaration, member access, initialization, and passing structs to functions.