Interview questions

C Add to Array: How to Build a Safe Dynamic Array

August 5, 2025Updated May 15, 202616 min read
Why 'C Add To Array' Isn't As Simple As You Think And How To Do It Right

Learn how to add to an array in C with a safe dynamic array pattern using malloc, realloc, length, and capacity — plus what to do when realloc fails.

Every C programmer hits the same wall: you declare an array, fill it up, and then realize you need one more slot. The question of how to C add to array shows up constantly in forums and classrooms because the syntax looks like it should be flexible — it isn't. C arrays are fixed at declaration time, full stop, and no amount of pointer arithmetic changes that. What you actually need is a wrapper that manages growth for you, and building one safely is the real skill worth learning here.

This guide skips the conceptual overview and goes straight to the implementation: a heap-backed struct with separate length and capacity fields, an append path that avoids unnecessary allocations, a realloc pattern that never gambles your only copy of the data, and a cleanup function that leaves nothing behind. By the end, you will have a working dynamic array design you can actually use.

Why C add to array is the wrong question

The phrase "add to array" implies the array has a back end you can push onto. It doesn't. Understanding why that assumption breaks is the fastest way to stop fighting the language and start using it correctly.

C arrays stop where they start

When you write `int nums[8];`, the compiler reserves exactly 32 bytes (on a 32-bit int platform) at a fixed address — either on the stack or in static storage. That address does not move. The size does not change. There is no hidden metadata tracking how full the array is, and there is no mechanism in the language to extend the allocation after the fact.

The C standard (ISO/IEC 9899) defines arrays as objects with a fixed element count determined at declaration or compile time. A variable-length array (VLA) introduced in C99 lets you set the size at runtime, but it still cannot grow after creation — the size is fixed the moment the declaration executes. Pointer arithmetic lets you walk the memory, but it does not conjure new memory that was never allocated.

The real danger is that C will not stop you from writing past the end of an array. It just corrupts whatever lives at the next address. That buffer overflow is one of the most common sources of undefined behavior in C programs, and it usually shows up as a crash somewhere completely unrelated to where the bad write happened — which makes it genuinely painful to debug.

What people mean when they ask for append

When someone asks how to add an element to an array in C, they almost always mean one of two things: either they have spare capacity they haven't used yet and they want to write the next element, or they have run out of room and they need the storage to grow. Those are different problems with different solutions.

The first case — spare capacity exists — is straightforward. Write to the next index and increment your length counter. The second case requires a different data structure entirely: something heap-backed that can be resized. A raw C array handles neither case automatically, because it tracks neither how much capacity exists nor how many elements are currently live. You have to bring that bookkeeping yourself.

Early in my own experience with C, I spent two hours chasing a segfault that turned out to be a stale length variable. The array had been reallocated, the pointer updated, but the length counter was still pointing at the old size. The write landed in valid memory but at the wrong offset. The bug looked random. It wasn't — it was a bookkeeping failure, and it was entirely self-inflicted.

Build the wrapper: length, capacity, and a heap pointer

A dynamic array in C is not a language feature. It is a design pattern: a struct that wraps a heap pointer and carries the metadata the pointer alone cannot express.

Start with the smallest usable struct

You need three things and nothing more to start:

`data` is the heap pointer to the actual storage. `length` is how many elements have been written. `capacity` is how many elements the current allocation can hold. The pointer alone tells you where the data lives. It does not tell you how much room is left or how many elements are valid. Without `length` and `capacity`, every append becomes a guess.

This design mirrors what production C libraries use. The GLib dynamic array (GArray) tracks length and a separate allocation size for exactly this reason. The pattern is not academic — it is the standard approach in real systems code.

What this looks like in practice

Initialization allocates an initial buffer and sets both counters to honest values:

`length` starts at zero because no elements have been written yet. `capacity` starts at whatever you passed in — commonly 8 or 16 for general-purpose use. The malloc failure check is not optional. On embedded targets or under memory pressure, `malloc` can return `NULL`, and proceeding with a null `data` pointer guarantees a crash on the first write.

Notice that the struct itself does not own any memory until `array_init` runs. If you pass an uninitialized `IntArray` to an append function, `data` is garbage. Discipline about initialization is what separates code that works from code that works until it doesn't.

Append into spare capacity before you reach for realloc

The cheap path for how to add to array in C is the one that does not allocate at all. If capacity already covers the next element, you do not need realloc — you just write and update the counter.

The cheap path works when there is room

An append function should check capacity first. If `length < capacity`, the write is safe:

That is the entire operation. Two lines. The bug that kills this pattern is almost always forgetting one of those two steps — either writing to the right index but not incrementing `length`, which means the next append overwrites the same slot, or incrementing `length` before writing, which shifts the write position forward by one and leaves a gap.

What this looks like in practice

Say the array has capacity 8 and currently holds three elements (indices 0, 1, 2). `length` is 3. `capacity` is 8.

Zero-based indexing means the next open slot is always at `arr->data[arr->length]`. If length is 4, the next write goes to index 4. If you mistakenly use `arr->data[arr->length - 1]`, you overwrite the last valid element. This is the kind of off-by-one that passes all your happy-path tests and then fails silently the first time you append to an empty array.

According to cppreference.com's coverage of array subscripting, array elements in C are zero-indexed and stored contiguously in memory — which is exactly why `arr->data[arr->length]` is always the correct write position for the next append, as long as capacity permits.

Use realloc without gambling the original pointer

When `length` reaches `capacity`, you need more memory. This is where realloc in C enters the picture — and where the most dangerous mistake in dynamic array code lives.

The failure mode that quietly breaks programs

The naive pattern looks like this:

This is wrong. If `realloc` fails, it returns `NULL` — and you just overwrote `arr->data` with `NULL`. The original buffer is now leaked: the memory is still allocated, but you have lost the only pointer to it. The program cannot free it, cannot read from it, and will crash the moment it tries to use `arr->data`.

This is not a theoretical edge case. On embedded systems, under memory pressure, or during a test that deliberately exhausts the heap, `realloc` will fail. Programs that use the naive pattern will corrupt their own state silently and then crash somewhere else, leaving a debugging trail that points nowhere useful.

What this looks like in practice

The safe pattern uses a temporary pointer:

`tmp` holds the result of `realloc`. If it is `NULL`, the function returns an error and `arr->data` still points to the original allocation. The caller can decide what to do — retry, return an error upstream, or abort. The original data is intact. If `tmp` is non-null, the resize succeeded, and only then does `arr->data` get updated.

The Linux man page for realloc is explicit on this point: if `realloc` returns `NULL`, the original pointer is unchanged and the original allocation is still valid. The safe pattern is designed around this guarantee — the temporary pointer is what makes it possible to honor it.

Why capacity doubling keeps the code sane

Doubling capacity on each resize is a deliberate performance choice. If you grew by one slot on every append, you would call `realloc` on every single append past the initial capacity. Each `realloc` may copy the entire buffer to a new address. For an array of N elements, that means O(N²) copy operations total.

Doubling means the capacity grows like this: 8 → 16 → 32 → 64 → 128. After 128 elements, you have called `realloc` only 4 times since the initial allocation of 8. The total copy work across all those resizes is O(N) amortized — each element has been copied at most log₂(N) times. That is why nearly every production dynamic array implementation, from C++ `std::vector` to Python's list internals, uses a growth factor near 2.

The tradeoff is memory overhead: at worst, half your capacity is unused. For most applications, that is a fine price. For embedded systems with hard memory ceilings, it may not be — which is why the decision tree matters.

Free the memory cleanly or the whole pattern falls apart

You can resize array in C correctly and still leak memory if cleanup is an afterthought. The heap allocation the wrapper manages has to be explicitly freed — nothing in C does it for you.

Free the buffer, not just the struct

If your `IntArray` is stack-allocated (the struct itself lives on the stack), the struct's fields will be reclaimed automatically when it goes out of scope. The heap memory `data` points to will not be. That allocation lives until you call `free` on it or the process exits. In a long-running program, or one that creates and destroys many arrays, that leak adds up.

What this looks like in practice

`free(arr->data)` releases the heap buffer. Setting `arr->data` to `NULL` afterward is a defensive habit: it makes a double-free detectable (freeing a null pointer is a no-op in C, so the second call will not crash) and it prevents use-after-free bugs where code accidentally reads from `arr->data` after cleanup. Resetting `length` and `capacity` to zero ensures the struct is in a consistent, unusable-but-honest state. If code tries to append after `array_destroy`, the capacity check will immediately trigger a grow attempt — which will fail cleanly if you also check for a null `data` pointer in `array_grow`.

Stop the usual bugs before they ship

A working dynamic array in C is not just about getting the happy path right. The bugs that survive code review are almost always bookkeeping failures in the edge cases.

The off-by-one that eats the next write

The most common mistake is conflating length, capacity, and index in the same expression. Length is a count. Capacity is a count. Index is a position. They are related but not interchangeable:

  • `arr->data[arr->length]` — correct write position for the next element
  • `arr->data[arr->length - 1]` — the last valid element (off-by-one if you meant the next slot)
  • `arr->capacity - 1` — the last valid index if the array were full (not the same as `arr->length - 1` when the array is partially filled)

When zero-based indexing meets manual bookkeeping, the error usually shows up as a silent overwrite: the program does not crash, it just stores data in the wrong slot, and the corruption surfaces later as a logic error that looks unrelated.

Stale length and leaked memory are usually self-inflicted

Two failure patterns appear over and over in C code reviews:

Stale length happens when the buffer is reallocated (capacity updated, data pointer updated) but `length` is not touched. The length still reflects the old count, which is correct — but if the realloc was triggered by a grow path that also wrote a new element, and the length increment was skipped, every subsequent append overwrites the same slot.

Leaked old allocations happen when code manually manages two pointers during a resize and forgets to free the old one before replacing it. With the safe `realloc` pattern above, this does not happen — `realloc` handles the old buffer internally. But if you ever write a manual `malloc`-then-copy-then-free resize, the free step is the one that gets skipped under pressure.

Tools like Valgrind and AddressSanitizer catch both of these reliably. Running them on any C code that manages heap memory is not optional — it is the minimum bar for calling the code correct.

Use a fixed array when growth is the wrong job

Not every problem needs a dynamic array. Reaching for `realloc` when a fixed buffer would do is its own kind of mistake — it adds complexity, introduces failure paths, and wastes memory on systems that cannot afford it.

Embedded code does not always want flexibility

On microcontrollers and real-time systems, heap allocation is often forbidden outright. Dynamic memory on constrained hardware introduces fragmentation, non-deterministic allocation times, and failure modes that are hard to test exhaustively. Many embedded coding standards (MISRA C, for example) restrict or prohibit `malloc` entirely.

In those environments, the right answer for how to add to array in C is a fixed-size buffer with a length counter and a hard ceiling check:

If `length == LOG_CAPACITY`, you do not grow — you either drop the entry, overwrite the oldest (ring buffer), or signal an error. The behavior is bounded, deterministic, and requires no heap.

What this looks like in practice

The decision rule is simple:

  • Fixed buffer: memory is tight, the maximum size is known at compile time, or dynamic allocation is prohibited. Use a stack or statically allocated array with a length counter and a bounds check.
  • Dynamic wrapper: you genuinely cannot predict the maximum size, or the data can grow large enough that a worst-case fixed allocation would be wasteful. Use the heap-backed struct with `malloc`, `realloc`, and a cleanup function.

Most of the bugs that come from "I'll just use a dynamic array for everything" are really cases where the upper bound was knowable and a fixed buffer would have been simpler, faster, and safer. The wrapper is a tool, not a default.

FAQ

Q: Can you append to an array in C, or do you need a different data structure?

You cannot append to a fixed C array — its size is set at declaration and cannot change. If you need true append semantics, you need a heap-backed wrapper struct with a separate length and capacity field, using `malloc` for initial allocation and `realloc` when the capacity is exhausted. The raw array is a building block, not a complete data structure.

Q: What is the safe way to add an element when you already know there is spare capacity?

Write the value to `arr->data[arr->length]` and then increment `arr->length`. That is the entire operation. The common bug is doing one without the other — writing without incrementing leaves the next append overwriting the same slot, and incrementing before writing shifts the position forward by one and leaves a gap.

Q: How do you implement a resizable array in C with malloc and realloc?

Define a struct with a `data` pointer, a `length`, and a `capacity`. Initialize with `malloc` and set both counters honestly. When `length == capacity`, call `realloc` into a temporary pointer, check for `NULL`, and only update `data` and `capacity` after a successful resize. Double the capacity on each grow to keep amortized append cost at O(1). Free the data pointer when the array is no longer needed.

Q: What should you do if realloc fails so you do not lose the original buffer?

Assign the result of `realloc` to a temporary pointer, not directly to `arr->data`. If the temporary pointer is `NULL`, `realloc` failed — return an error to the caller and leave `arr->data` untouched. The original buffer is still valid and the data is intact. If you assign `realloc`'s result directly to `arr->data` and it returns `NULL`, you have overwritten the only pointer to the original allocation and leaked it permanently.

Q: How do length and capacity differ in a C array wrapper?

Length is how many elements have been written and are currently valid. Capacity is how many elements the current allocation can hold. When `length < capacity`, appending is free — just write and increment. When `length == capacity`, you need to grow the allocation before the next write. The pointer alone carries neither of these values, which is why the wrapper struct exists.

Q: What should an interview-ready answer say about adding to an array in C?

A strong answer covers three things: first, that fixed C arrays cannot grow in place and the solution is a heap-backed struct; second, that the struct needs separate length and capacity fields because the pointer alone is insufficient; and third, that the safe realloc pattern uses a temporary pointer to preserve the original buffer on failure. Bonus points for mentioning capacity doubling and the cleanup requirement. Interviewers asking this question are checking whether you understand memory ownership, not just syntax.

Q: What is the safest approach for embedded C when memory growth is constrained?

Use a fixed-size buffer with a compile-time capacity constant and a length counter. Check the length against the capacity before every write and handle the full-buffer case explicitly — drop the entry, overwrite the oldest, or return an error. Avoid `malloc` and `realloc` on systems where heap allocation is restricted, non-deterministic, or prohibited by your coding standard. The constraint is a feature, not a limitation: bounded memory behavior is easier to test and reason about than unbounded growth.

Conclusion

C does not give you a growable array. It gives you contiguous memory, a pointer, and the responsibility to manage everything else yourself. That is not a flaw in the language — it is the contract. The question is whether you meet it with a pattern that is actually safe.

The pattern that works is not complicated: a struct with a heap pointer, a length, and a capacity; an append path that writes to the next index and increments the counter; a realloc path that uses a temporary pointer so a failed resize never loses the original data; and a destroy function that frees the buffer and resets the fields. Each piece earns its place. Remove any one of them and you get a bug that will eventually surface in production at the worst possible moment.

If you need append, build the wrapper. Track length and capacity separately. Never assign `realloc`'s return value directly to the pointer you are trying to grow. Free what you allocate. And if you are working on embedded hardware where memory is bounded and predictable, reach for the fixed buffer instead — the dynamic wrapper is a tool for genuine uncertainty, not a default for every array in your codebase.

VA

Verve AI

Interview Guidance

Ace your live interviews with AI support!

Get Started For Free

Available on Mac, Windows and iPhone