Hello! Welcome back to our series on C pointers.
In our last lesson, we tackled some of the most serious memory management errors: use-after-free and double-free. We established that the discipline of nullifying a pointer immediately after freeing the memory it points to (free(ptr); ptr = NULL;) is a powerful defense against these dangerous, unpredictable bugs. You also saw why safety-critical standards like MISRA C often forbid dynamic allocation entirely.
Today, we'll shift from preventing errors to building something practical and powerful. We will synthesize everything we've learned about malloc(), realloc(), and free() to achieve today's learning outcome: to implement a dynamic array data structure.
This is a foundational data structure, known in C++ as std::vector, that you have likely used in higher-level forms. Building it from scratch in C is an excellent way to solidify your understanding of manual memory management. For you, this exercise will connect the low-level mechanics of pointers and memory with the practical challenge of creating reusable, flexible software components.
What is a Dynamic Array?
A standard C array has a fixed size determined at compile time. A dynamic array, in contrast, is an array that can grow or shrink at runtime. To implement this, we need to manage a block of memory on the heap and keep track of two key properties:
- Logical Size (or
size): The number of elements currently stored in the array. - Capacity: The total number of elements the currently allocated memory block can hold.
The core idea is that we often allocate more memory than we immediately need. When the logical size reaches the capacity, we allocate a new, larger block of memory, copy the old data over, and then free the old block. This process is efficiently handled by realloc().
Let's begin by reading a short article that visualizes this concept.
Dynamic Array | Brilliant Math & Science Wiki
The article 'Dynamic Array' from Brilliant provides a clear definition of logical size versus capacity and includes an excellent diagram showing how an array grows as elements are appended.
Please read the 'Properties of the Dynamic Array' section. Focus on the definitions of 'capacity' and 'logical size' and study the diagram that shows an array being initialized and then expanding as new values are added.
Designing the Dynamic Array Structure
To manage our dynamic array, we need to bundle the data pointer, the logical size, and the capacity together. A struct is the perfect tool for this. A well-designed dynamic array should also be generic, meaning it can hold any data type. We can achieve this by storing the size of each item.
Here is a common and robust design for a dynamic array structure in C:
typedef struct {
void *data; // Pointer to the block of memory on the heap
size_t itemSize; // The size of a single item (e.g., sizeof(int))
size_t size; // The current number of items (logical size)
size_t capacity; // How many items the allocated memory can hold
} DynamicArray;
With this structure, we can create a set of functions to initialize the array, add elements, and clean it up.
The Core Logic: Appending and Resizing
The most critical operation is appending a new element. This is where the "dynamic" nature comes into play. The logic is as follows:
- Check if the array is full (
array->size == array->capacity). - If it is full, calculate a new, larger capacity. A common strategy is to double the current capacity.
- Use
realloc()to resize the memory block pointed to bydata. - Once there is enough capacity, copy the new element to the end of the array.
- Increment the
size.
The following short video demonstrates this core resizing logic in a very direct, code-focused way.
The video 'Dynamic Arrays in C' by Tsoding quickly demonstrates the fundamental logic of checking capacity and using realloc to grow an array when it's full.
Watch the first minute of the video (00:10 - 01:05). You'll see the implementation of the if (count >= capacity) check, the calculation of a new capacity, and the call to realloc. This is the essential algorithm we are building.
A Safer realloc Pattern
One crucial detail when using realloc() is handling potential allocation failures. realloc() returns NULL if it cannot resize the memory. If you assign this NULL back to your original pointer, you lose your only reference to the old memory block, creating a memory leak.
Unsafe Pattern:
// If realloc fails, arr becomes NULL and the original memory is leaked.
arr = realloc(arr, new_size);
Safe Pattern:
// Use a temporary pointer.
int *temp = realloc(arr, new_size);
if (temp == NULL) {
// Handle the error: maybe free the original memory and exit.
free(arr);
// ... handle failure ...
} else {
// Only update the pointer on success.
arr = temp;
}
This safe pattern is essential for writing robust code, a principle you're very familiar with from your work in functionally safe systems.
The internal mechanism of realloc is also interesting. If it can expand the memory block in-place, it will. If not, it finds a new, larger block, copies the data from the old block to the new one, and frees the old block for you.
A Complete, Generic Implementation
Now, let's put it all together. The Brilliant.org article we looked at earlier provides a full, clean, and generic implementation of a dynamic array. It uses void* and memcpy to handle items of any size, which is a common and powerful pattern in C.
Dynamic Array | Brilliant Math & Science Wiki
We'll now study the full source code for a generic dynamic array. This example uses a header file for the public interface and a source file for the implementation, which is excellent practice.
Please review the code under the 'Sample C implementation' section, including DynamicArray.h, DynamicArray.c, and main.c. As you read, focus on: DynamicArray.h: How it defines the struct and the function prototypes that form the public API. DynamicArrayCreate: How it allocates memory for both the DynamicArray struct itself and the internalArray data buffer. DynamicArrayAppend: How it implements the resizing logic using realloc and a growth factor. Notice how it calls DynamicArraySet to place the new element. DynamicArrayGet and DynamicArraySet: How they use pointer arithmetic (internalArray + indexMemoryOffset) and memcpy to work with any data type. DynamicArrayDispose: How it correctly frees both the internalArray and the DynamicArray struct.
Dynamic Arrays of Structs and Nested Allocations
The generic implementation is powerful, but it's also helpful to see a concrete example with a more complex data type, like a struct. Your work in automotive systems often involves managing complex data structures for CAN frames, service-oriented communication, or ADAS sensor data.
The following video demonstrates creating a dynamic array of structs and brings up a critically important point about cleanup.
Dynamically Allocate An Array Of Structs | C Programming Tutorial
The video 'Dynamically Allocate An Array Of Structs' will walk us through creating and resizing an array of a custom point struct. It concludes with an essential topic for complex data structures.
Please watch these three segments: Allocation and Access (04:08 - 09:01): See how malloc is used with sizeof(point) * length to allocate the array. Pay attention to how individual struct members are accessed using array[i].x. Resizing with realloc (09:01 - 11:15): Observe the use of realloc to increase the array's size to accommodate more structs. Crucial Cleanup (11:15 - 14:48): This is the most important part. The point struct is modified to include a char* for a description, which is also dynamically allocated. Watch carefully to see why you must first loop through the array to free each individual description string before you can free the array itself. This prevents significant memory leaks.
The final point in the video is paramount. If your data structure contains pointers to other dynamically allocated memory, you are responsible for freeing that "inner" memory before freeing the "outer" container. A single free(array) call is not enough. This principle of ownership and cleanup is fundamental to managing complex resources in C.
Test your understanding!
Based on the generic DynamicArray implementation from the Brilliant.org article, imagine you need to add a DynamicArrayShrinkToFit function. Its purpose is to reduce the array's capacity to match its current logical size, freeing up unused memory.
void DynamicArrayShrinkToFit(DynamicArray *dynamicArray);
How would you implement this function? Consider the following points:
- What condition should you check before doing anything?
- Which function (
malloc,realloc,free) would you use? - How would you apply the "safe allocation" pattern?
- What fields of the
DynamicArraystruct would you need to update?
Show answer
Here is a possible implementation for DynamicArrayShrinkToFit:
void DynamicArrayShrinkToFit(DynamicArray *dynamicArray) {
// 1. Only act if capacity is greater than size.
if (dynamicArray->capacity > dynamicArray->size) {
// Handle the edge case of shrinking to zero.
if (dynamicArray->size == 0) {
free(dynamicArray->internalArray);
dynamicArray->internalArray = NULL; // Good practice!
} else {
// 2. Use realloc with the safe pattern.
void *temp = realloc(dynamicArray->internalArray, dynamicArray->size * dynamicArray->itemSize);
// 3. Check for realloc failure. In this case, we can just keep the old, larger buffer.
if (temp != NULL) {
dynamicArray->internalArray = temp;
}
// If realloc fails, we simply fail to shrink, which is acceptable.
}
// 4. Update the capacity to match the new size.
dynamicArray->capacity = dynamicArray->size;
}
}
This implementation uses realloc to request a smaller memory block and updates the capacity field to reflect the change, ensuring the struct's state remains consistent.
Conclusion
In this lesson, you've built a complete, non-trivial data structure in C from first principles. This is a significant step in mastering pointers and memory management. You have moved beyond simply using allocation functions to designing a system that encapsulates memory logic, which is the foundation of creating robust and scalable C programs.
Key Takeaways:
- A dynamic array is implemented using a pointer to heap memory, along with
sizeandcapacitycounters managed in astruct. - The
appendoperation's core logic involves checking ifsize == capacity, and if so, usingreallocto expand the memory buffer. - Safe
reallocusage involves a temporary pointer to prevent memory leaks if allocation fails. - A generic dynamic array can be created using
void*pointers,itemSizetracking, andmemcpyfor data manipulation. - When managing arrays of complex structures, you must ensure that any nested dynamically allocated memory within those structures is freed before freeing the array itself.
Preview of the Next Lesson:
We've implemented a functional dynamic array, but we only briefly touched on the safe realloc pattern. For code to be truly production-ready, especially in the high-stakes environment of automotive software, error handling must be systematic and robust. In our next lesson, we will focus exclusively on how to handle memory allocation failures and write robust error-checking code, turning our prototype-level functions into hardened, reliable components.