pointers-memory Lesson 8 20 min read

How Do I Resize Dynamic Arrays and Copy Memory?

realloc, memcpy, memmove — growing arrays at runtime like ArrayList does behind the scenes

Reading: C Text: Ch. 13 §2 (realloc), Ch. 8 §3 (memcpy/memmove from string.h)

After this lesson, you will be able to:

  • Use realloc to 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 memcpy for non-overlapping copies and memmove when 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 NULL on failure — but the original block is still valid

Common Pitfall: Never write arr = realloc(arr, new_size). If realloc fails and returns NULL, you’ve lost the original pointer — memory leak! Always use a temporary variable to check first.

Check Your Understanding
What happens when realloc can't expand a block in place?
A It returns NULL and frees the old block
B It crashes with a segfault
C It returns the same pointer and silently does nothing
D It allocates a new block, copies the data, and frees the old block
Answer: D. 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 ArrayList does. When the internal array fills up, ArrayList allocates a new array ~1.5x larger, copies everything over, and discards the old one. realloc gives you the same behavior — but you manage it yourself.

Check Your Understanding
What's wrong with this growth code?

arr = realloc(arr, new_capacity * sizeof(int));
A You need to call free(arr) first before realloc
B If realloc returns NULL, the original pointer is lost — memory leak
C realloc can't resize — it only allocates new blocks
D Missing cast — realloc returns void * which must be cast
Answer: B. If 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:

  1. Allocates a new array of size n + 1 with calloc
  2. Copies all n elements from the old array to the new one
  3. Adds the new element at position n
  4. Frees the old array
  5. 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 realloc and memcpy, 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.