Build Your Own std::vector in C

Over the years of working with C, I often found myself stuck when dealing with dynamically resizable data structures. More often than not, I’d end up allocating a huge fixed-size array which worked at first but soon became a pain to maintain. Tracking index positions, avoiding segmentation faults, and ensuring I wasn’t accessing non-existent memory turned into a nightmare. That’s when I thought: why not add some training wheels to this tough ride?

In C++, std::vector is the go-to dynamic array which grows as you push elements. But in good old C, you have to manage that memory yourself.

So let’s learn how to build our own mini std::vector using malloc(), realloc(), and a little C magic. This isn’t the perfect recipe, so you might add salt and pepper suitable to your taste.

So to begin with, we’ll first prepare our core structure.

The Core Structure

We’ll have:

  • a pointer to the data,
  • the current size,
  • the capacity,
  • and the size of each element.
typedef struct {
    void *data;
    size_t element_size;
    size_t size;
    size_t capacity;
} Vector;

Here I have used a void* for the “data”. This will make it generic and we’ll be able to use pretty much all data types available.

Initialization of the Vector

void vector_init(Vector *vec, size_t element_size) {
    vec->element_size = element_size;
    vec->size = 0;
    vec->capacity = 4;
    vec->data = malloc(vec->capacity * element_size);
    if (!vec->data) {
        perror("malloc failed");
        exit(EXIT_FAILURE);
    }
}

In the init function, we will be needing the reference to the struct Vector and the size of the individual element which we will be storing in the Vector. We use malloc here to allocate some initial capacity for our Vector.

Add (Push) an Element

Now after the initialization, the next function that we need would be the push function.

void vector_push_back(Vector *vec, const void *element) {
    if (vec->size == vec->capacity) {
        vec->capacity *= 2;
        void *new_data = realloc(vec->data, vec->capacity * vec->element_size);
        if (!new_data) {
            perror("realloc failed");
            free(vec->data);
            exit(EXIT_FAILURE);
        }
        vec->data = new_data;
    }

    // Copy element into the array
    void *target = (char *)vec->data + (vec->size * vec->element_size);
    memcpy(target, element, vec->element_size);
    vec->size++;
}

The vector_push_back() function is the heart of our dynamic array.
It allows the array to grow automatically when it runs out of space, similar to how std::vector::push_back() works in C++.

First thing we do is check if there’s space in vector. When size equals capacity, the vector is full, and we must increase its capacity before inserting another element.

Grow the Vector with realloc()

vec->capacity *= 2;
void *new_data = realloc(vec->data, vec->capacity * vec->element_size);

This is the key part of the function.

What realloc() Does

  • realloc() stands for reallocate memory.
  • It is used to resize an already allocated block of memory.
  • It tries to expand the current block in place. If that is not possible, it allocates a new, larger block elsewhere, copies the existing data into it, and frees the old one automatically.

In our case:

  1. We double the capacity (for example, from 4 to 8, then 8 to 16, and so on).
  2. We then call realloc() to request a larger block of memory that can hold all elements.

If the reallocation is successful, we get back a pointer (new_data) to the resized block.

Memory allocation can fail, especially if the system is low on available RAM.
If that happens, realloc() returns NULL.

Here, we:

  • Print an error message using perror(),
  • Free the old memory to avoid leaks,
  • And exit the program safely.

Once we have a successfully reallocated memory block, we update vec->data so it points to the new (possibly moved) memory location.

Copy the New Element into the Array

Here’s what’s happening:

  • We cast vec->data to a char* so that we can do byte-level pointer arithmetic (since void* arithmetic isn’t allowed in C).
  • vec->size * vec->element_size gives us the correct byte offset where the new element should be placed.
  • target now points to the exact memory location for the new element.

The memcpy() function then copies the bytes from the element we want to insert into this target location.

Finally, we increase the element count, since we’ve just added one more value to the vector.

Accessing Elements from the Vector

We return a pointer to the element at the given index.

void* vector_get(Vector *vec, size_t index) {
    if (index >= vec->size) {
        fprintf(stderr, "Index out of bounds\n");
        exit(EXIT_FAILURE);
    }
    return (char *)vec->data + (index * vec->element_size);
}

This function retrieves an element’s address from the vector.
It first checks whether the requested index is valid (within the current size).
If the index is out of range, it prints an error and terminates the program to prevent invalid memory access. Otherwise, it calculates the element’s memory address using pointer arithmetic and returns it.

Free the Vector

Now this is a bit unique to C, as we need to free memory allocated memory before exit.

void vector_free(Vector *vec) {
    free(vec->data);
    vec->data = NULL;
    vec->size = 0;
    vec->capacity = 0;
    vec->element_size = 0;
}

Example Usage

Now that we have built our Vector completely, let’s put it to use in the main

Vector vec;
vector_init(&vec, sizeof(int));

for (int i = 0; i < 10; ++i) { vector_push_back(&vec, &i); } for (size_t i = 0; i < vec.size; ++i) { int *val = vector_get(&vec, i); printf("%d ", *val); } printf("\n"); vector_free(&vec); return 0; [/c]

So, finally this is what it looks like all together.

#include
#include
#include

typedef struct {
void *data; // pointer to the data
size_t element_size; // size of each element
size_t size; // number of elements stored
size_t capacity; // allocated capacity
} Vector;

typedef struct { char name[20]; int age; } Person;

void vector_init(Vector *vec, size_t element_size) {
vec->element_size = element_size;
vec->size = 0;
vec->capacity = 4;
vec->data = malloc(vec->capacity * element_size);
if (!vec->data) {
perror(“malloc failed”);
exit(EXIT_FAILURE);
}
}

void vector_push_back(Vector *vec, const void *element) {
if (vec->size == vec->capacity) {
vec->capacity *= 2;
void *new_data = realloc(vec->data, vec->capacity * vec->element_size);
if (!new_data) {
perror(“realloc failed”);
free(vec->data);
exit(EXIT_FAILURE);
}
vec->data = new_data;
}

// Copy element into the array
void *target = (char *)vec->data + (vec->size * vec->element_size);
memcpy(target, element, vec->element_size);
vec->size++;
}

void* vector_get(Vector *vec, size_t index) {
if (index >= vec->size) {
fprintf(stderr, “Index out of bounds\n”);
exit(EXIT_FAILURE);
}
return (char *)vec->data + (index * vec->element_size);
}

void vector_free(Vector *vec) {
free(vec->data);
vec->data = NULL;
vec->size = 0;
vec->capacity = 0;
vec->element_size = 0;
}

int main() {
Vector vec;
vector_init(&vec, sizeof(int));

for (int i = 0; i < 10; ++i) { vector_push_back(&vec, &i); } for (size_t i = 0; i < vec.size; ++i) { int *val = vector_get(&vec, i); printf("%d ", *val); } printf("\n"); vector_free(&vec); return 0; } [/c]

In this code, I have created another struct named Person, as an exercise you could try using that struct with our newly created vector.

Hopefully this helps.