How Do I Resize Dynamic Arrays and Copy Memory?
realloc, memcpy, memmove — growing arrays at runtime like ArrayList does behind the scenes
After this lesson, you will be able to:
- Use
reallocto resize a dynamic array with a temporary variable for safety - Implement the double-when-full growth pattern for dynamically sized arrays
- Explain why
arr = realloc(arr, new_size)is unsafe - Use
memcpyfor non-overlapping copies andmemmovewhen regions may overlap
What Happens When Your Array Is Full?
You allocated room for 10 scores. The user enters an 11th. Now what?
You can’t just write to arr[10] — that’s a buffer overflow. You need to allocate a bigger block, copy the data over, and free the old one. That’s exactly what realloc does.
Growing and Copying Memory
realloc: Resize an Allocation
int *arr = calloc(10, sizeof(int));
// ... fill 10 elements ...
// Need more room:
int *temp = realloc(arr, 20 * sizeof(int));
if (temp == NULL)
{
// realloc failed — arr is still valid!
fprintf(stderr, "Realloc failed\n");
}
else
{
arr = temp; // Update pointer to new (possibly different) location
}
realloc(ptr, new_size) attempts to resize the block:
- If there’s room to expand in place, it does (fast)
- If not, it allocates a new block, copies the data, and frees the old block
- Returns
NULLon failure — but the original block is still valid
Common Pitfall: Never write
arr = realloc(arr, new_size). Ifreallocfails and returns NULL, you’ve lost the original pointer — memory leak! Always use a temporary variable to check first.
realloc can't expand a block in place?realloc tries to extend in place first (fast path). If it can't, it allocates a new larger block, copies all the old data over, frees the old block, and returns the new address. This is why the returned pointer might differ from the original — you must always update your pointer. Option A is a common misconception: on failure, realloc returns NULL but the old block stays valid.
The Double-When-Full Pattern
The standard pattern for growable arrays:
int capacity = 4;
int size = 0;
int *arr = calloc(capacity, sizeof(int));
// When adding an element:
if (size >= capacity)
{
capacity *= 2;
int *temp = realloc(arr, capacity * sizeof(int));
if (temp == NULL)
{
fprintf(stderr, "Out of memory\n");
free(arr);
exit(1);
}
arr = temp;
}
arr[size] = new_value;
size++;
From Java: This is exactly what
ArrayListdoes. When the internal array fills up,ArrayListallocates a new array ~1.5x larger, copies everything over, and discards the old one.reallocgives you the same behavior — but you manage it yourself.
arr = realloc(arr, new_capacity * sizeof(int));realloc fails, it returns NULL — and you've just overwritten arr with NULL. The original memory block is still allocated but you've lost the pointer to it. Always assign to a temp variable: int *temp = realloc(arr, ...); if (temp != NULL) arr = temp;. Option A is wrong — realloc handles the old block internally. Option D is wrong — in C (not C++), void * converts to any pointer type implicitly.
Why does this matter?
The double-when-full pattern is one of the most important algorithms in all of CS. It gives you amortized O(1) appends — the same performance guarantee behind Java’s ArrayList, Python’s list, and Rust’s Vec. You’re not just using it; you’re building it yourself.
Deep dive: Lab 6's copy-and-resize approach vs. realloc
Lab 6 uses an alternative to realloc for adding elements. Instead of resizing in place, the addItem function:
- Allocates a new array of size
n + 1withcalloc - Copies all
nelements from the old array to the new one - Adds the new element at position
n - Frees the old array
- Returns the new array pointer
int *addItem(int *length, int *array)
{
int value;
printf("Enter value to add: ");
scanf("%d", &value);
while (fgetc(stdin) != '\n') {}
int *newArr = (int *)calloc(*length + 1, sizeof(int));
if (newArr == NULL) { perror("calloc"); free(array); exit(-1); }
for (int i = 0; i < *length; i++)
{
newArr[i] = array[i];
}
newArr[*length] = value;
free(array);
(*length)++;
return newArr;
}
This is conceptually clear but costs O(n) per add (copy everything each time). The realloc approach can sometimes resize in place (faster). Java’s ArrayList uses a doubling strategy — when it runs out of room, it doubles the capacity, giving O(1) amortized cost per add.
| Strategy | Cost per add | Total for n adds | Used by |
|---|---|---|---|
Allocate new n+1 each time |
O(n) | O(n²) | Lab 6 (clarity-focused) |
realloc with doubling |
O(1) amortized | O(n) | Professional C code |
Java ArrayList (1.5x growth) |
O(1) amortized | O(n) | Java standard library |
All three are valid — Lab 6 uses the copy approach for clarity. When performance matters, use the doubling pattern.
Notice the function signature: int *addItem(int *length, int *array). The length parameter is a pointer (pass-by-pointer from Week 6) because the function needs to modify the caller’s length variable. The function returns the new array pointer because the old one has been freed — always assign the return value immediately: array = addItem(&length, array);.
memcpy: Copy Memory Blocks
#include <string.h>
int src[] = {1, 2, 3, 4, 5};
int dest[5];
memcpy(dest, src, 5 * sizeof(int)); // Copy 20 bytes from src to dest
memcpy(dest, src, n_bytes) copies n_bytes from src to dest. It’s fast (often optimized with hardware instructions) but source and destination must not overlap.
memmove: Copy with Overlap
When source and destination overlap, use memmove:
int arr[] = {1, 2, 3, 4, 5};
// Shift elements right by 1 (overlapping!)
memmove(&arr[1], &arr[0], 4 * sizeof(int));
arr[0] = 0;
// arr is now {0, 1, 2, 3, 4}
memmove handles overlap correctly; memcpy doesn’t.
New Memory from realloc
When realloc expands a block, the new bytes are not initialized. If you need them zeroed:
int old_capacity = capacity;
capacity *= 2;
int *temp = realloc(arr, capacity * sizeof(int));
if (temp != NULL)
{
arr = temp;
// Zero the new portion:
memset(arr + old_capacity, 0, (capacity - old_capacity) * sizeof(int));
}
Quick Check: Why should you never write arr = realloc(arr, size)?
If realloc fails and returns NULL, the assignment overwrites arr with NULL — you lose the pointer to the original (still valid) block, causing a memory leak. Always assign to a temporary variable first.
Quick Check: What's the difference between memcpy and memmove?
memcpy is faster but requires that source and destination don’t overlap. memmove handles overlapping regions correctly. If there’s any chance of overlap, use memmove.
Quick Check: Why double the capacity instead of adding 1?
Adding 1 each time means you realloc on every insertion — O(n) copies each time, O(n²) total. Doubling means fewer reallocations — the total cost is amortized O(n). This is the same strategy ArrayList uses.
You’ve Built ArrayList
The double-when-full pattern with realloc is how every growable array implementation works — from Java’s ArrayList to Python’s list to Rust’s Vec. You’ve now built it yourself, understanding every step.
Next: Valgrind — the tool that finds memory bugs you can’t see. Memory leaks, use-after-free, invalid reads — Valgrind catches them all.
Big Picture: With
reallocandmemcpy, you can build any dynamic data structure from scratch. The growable array you just built is the foundation for dynamic strings, stack implementations, and buffer management. In Series 4, you’ll use these same tools to build linked lists and manage complex struct hierarchies.