A fixed-size array is simple, but some programs do not know how many records they will receive. A dynamic array solves this by managing an allocated block together with a logical length and a capacity. The design is small enough for a beginner project and rich enough to teach allocation failure, overflow checks, ownership, and interface contracts.
This guide develops the design of a fictional collection of integer marks. It concentrates on the invariants and failure behaviour that make an implementation reliable. The array is not a replacement for understanding pointers; it is a practical structure in which those ideas become visible and testable.
Store length and capacity separately
Length is the number of elements currently in use. Capacity is the number of elements the allocated block can hold. A collection with length three and capacity eight contains three valid marks and five unused slots. Reading an unused slot as though it were a valid element is an application error even though the storage exists.
Keep the pointer, length, and capacity together in one structure. State the invariant that length never exceeds capacity. Define an empty state, such as a null data pointer with both counts zero. Every operation should preserve the invariants or report failure without corrupting the existing collection.
Define the public operations
A small interface might support initialisation, append, indexed access, removal, and destruction. Document whether access functions return values or borrowed pointers, and whether append can invalidate previous references. Keep the internal allocation details behind these operations so callers cannot independently change capacity and break the structure’s invariants.
Decide how failure is reported. Returning a success flag or a defined error result is clearer than silently dropping an append. Do not use a valid stored mark as an error sentinel. The caller needs to distinguish an empty collection, an invalid index, and an allocation failure.
Choose a growth policy
Growing by one element for every append can repeatedly move the existing contents. A geometric policy, such as increasing capacity by a factor when full, reduces the number of growth operations over a sequence of appends. The exact policy is a trade-off between spare storage and the cost of repeated allocation and copying.
For a learning implementation, explain the chosen initial capacity and growth rule. Do not present doubling as universally optimal. Embedded systems, strict memory budgets, and known workload limits may require another approach. A useful design documents the reason for its policy and the maximum size it is willing to support.
Check arithmetic before allocation
The allocator receives a byte count, while the collection stores an element count. Multiplying a large capacity by the size of an element can overflow the size type. Check whether the requested capacity is representable in bytes before calculating the allocation size. A small wrapped result can otherwise allocate much less storage than the program believes it owns.
The growth calculation itself also needs a limit check. If capacity is doubled, verify that the doubled value is representable and acceptable before using it. Define what happens when the requested collection size exceeds the supported maximum. Returning a clear failure is preferable to relying on arithmetic wraparound.
Use realloc with a temporary result
For a positive requested size, a failed realloc leaves the original allocation available. Store the result in a temporary pointer and update the owner only after success. Assigning directly to the sole owner can lose the original address on failure and make cleanup impossible. The realloc reference documents the function’s behaviour.
Avoid using a zero-size resize as a portable destruction strategy. Explicitly release storage in the destructor and reset the structure’s fields according to its empty-state contract. This keeps cleanup separate from growth and avoids depending on version-specific or implementation-specific zero-size behaviour.
Commit state only after success
An append should first ensure sufficient capacity. If growth fails, leave the previous pointer, length, capacity, and elements unchanged. Only after storage is available should the new value be written and the logical length increased. This gives the caller a clear guarantee about what a failed operation means.
The same principle applies to more complex records. Construct a new element successfully before adding it to the active length, or define a cleanup path for partially constructed content. A collection that reports failure but has already modified its public state is difficult for callers to use correctly.
Understand pointer invalidation
Successful reallocation may move the block. Pointers into the old block must not be used afterward, even if a particular run appears to retain the same address. Document that growth invalidates borrowed element pointers. Callers should reacquire access after an operation that may resize the collection.
An index can be easier to retain than a raw pointer, but it also needs interpretation. Removing an earlier element can shift later positions. If the application needs a stable identity for a record, use a separate identifier and lookup rule rather than treating an array position as permanent identity.
Specify removal behaviour
Decide whether removal preserves order. Preserving order may require shifting later elements; replacing the removed element with the last element can be faster but changes order. The correct choice depends on what the application promises. A list of chronological marks should not unexpectedly reorder itself because an implementation shortcut was hidden from callers.
If elements own additional resources, release the removed element’s resources exactly once. For the simple integer exercise, no per-element cleanup is needed. State this simplification explicitly so learners do not copy the implementation unchanged into a structure containing allocated strings or open files.
Test the growth boundaries
Append to an empty array, fill exactly to capacity, and append once more to trigger growth. Verify that every previous value is preserved and length increases only after success. Test invalid indices, removal from an empty collection, removal of the first and last elements, and repeated destruction according to the documented contract.
Exercise allocation failure through a controlled test allocator or an equivalent supported technique. Do not attempt to exhaust the entire computer’s memory just to create a failure case. Verify that the original collection remains usable and can still be destroyed after a failed append.
Measure before claiming performance
A dynamic array offers contiguous storage and efficient indexed access, but its performance depends on workload and operations. Appending without growth differs from an append that moves a large block. Report those cases separately when measuring. Avoid claiming constant cost for every append without explaining the occasional growth work.
A beginner benchmark can compare several growth policies on the same synthetic input while recording allocation counts and total elapsed time. Keep the compiler settings and machine conditions consistent. Timing alone is not enough; first verify that each implementation produces the same correct values and handles failures properly.
Portfolio deliverable
Prepare an interface description, an invariant list, a growth-policy explanation, and a test matrix. Include a memory diagram before and after the first resize. Explain which references become invalid and how the caller learns that an operation failed. These artifacts demonstrate understanding beyond merely calling allocation functions.
Frequently asked questions
Is capacity the number of stored values?
No. Length counts active values, while capacity describes allocated element slots. The two may be equal, but they represent different facts.
Can realloc move the allocation?
Yes. Design callers so they do not keep using earlier pointers into the block after successful resizing.
Should every removal shrink the allocation?
Not necessarily. Frequent shrinking and regrowing can add work. Choose a policy based on workload and memory requirements, and document it.