Skip to Content
Course content

60: Practice Exercise: Building a Dynamic Array (Resizable Vector)

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen a lot of developers approach dynamic arrays by thinking of them as "magic arrays" that just expand whenever they need more room. Usually, this leads to a very specific, dangerous assumption: that the memory address of your array stays the same even after you resize it.

Thinking realloc just stretches your memory in place

Here is the scenario I see most often. A student creates a pointer to an element inside their array—maybe they're tracking the "current" item—and then they call realloc() to make the array bigger. They assume the array grew "downwards" into the empty space following the original block.

int *list = malloc(2 * sizeof(int));
int *current_item = &list[0]; // Pointing to the first element

// Now we need more space
list = realloc(list, 10 * sizeof(int));

// Danger! current_item might now be pointing to garbage memory
printf("Current item: %d", *current_item); 

The problem is that realloc is not guaranteed to grow your memory in place. If there isn't enough contiguous free space immediately following your current block, the operating system will find a new block of memory elsewhere, copy your data there, and free the old block. In the example above, current_item becomes a dangling pointer the second realloc moves the block. Your program might not crash immediately, but you've just introduced a Heisenbug that will haunt you for three days.

Managing capacity vs. size in a dedicated struct

To do this right, you have to stop thinking about the raw pointer and start thinking about the container. In C, the professional way to handle this is by wrapping your pointer in a struct. This allows you to track two different numbers: the size (how many elements are actually in there) and the capacity (how many elements the current memory block can hold before it must be resized).

I always recommend a geometric growth strategy—usually doubling the capacity. If you grow the array by only one element every time you push, your program will spend more time talking to the OS than actually processing data. Doubling ensures that as your list gets larger, you resize less and less frequently.

typedef struct {
    int *data;
    size_t size;
    size_t capacity;
} Vector;

void vector_init(Vector *v) {
    v->size = 0;
    v->capacity = 4; // Start small
    v->data = malloc(v->capacity * sizeof(int));
}

void vector_push(Vector *v, int value) {
    if (v->size == v->capacity) {
        v->capacity *= 2; // Geometric growth
        int *new_ptr = realloc(v->data, v->capacity * sizeof(int));
        if (new_ptr == NULL) {
            // Handle allocation failure—don't just overwrite v->data!
            return;
        }
        v->data = new_ptr;
    }
    v->data[v->size++] = value;
}

Notice how I used a temporary new_ptr. This is a non-negotiable habit. If realloc fails and returns NULL, and you've already assigned that NULL to v->data, you've just lost the pointer to your existing data, creating a massive memory leak. Always verify the new pointer before updating your struct.




📋 Practical Task

Exercise: Building a Robust Integer Vector with Bounds Checking

Your task is to complete the implementation of a dynamic integer array (Vector). You need to ensure the vector handles growth efficiently and prevents the user from accessing memory outside the allocated range.

Implement the following functions in a C file:

  • void vector_init(Vector *v): Initialize the vector with a starting capacity of 4.
  • void vector_push(Vector *v, int value): Add an element to the end. Double the capacity if the size reaches the capacity. Use a temporary pointer to handle realloc safely.
  • int vector_get(Vector *v, size_t index): Return the value at the given index. However, if the index is out of bounds (index >= v->size), print an error message to stderr and return a sentinel value (like -1).
  • void vector_free(Vector *v): Clean up the allocated memory and reset the struct members to zero.

Test Case: Create a vector, push 10 integers into it (forcing it to resize multiple times), retrieve the 5th element, and then free the memory. Ensure your program runs through Valgrind or a similar tool with zero memory leaks.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.