realloc and calloc

Work through examples, try the reasoning yourself, then put it into practice.

View Slides

Lesson

5 min read

Resize Without Losing Ownership

This module builds my_realloc and my_calloc on the supplied allocator. These are custom teaching APIs. In particular, my_realloc(p, 0) frees a live p and returns NULL; my_realloc(NULL, 0) returns NULL without allocating. Do not copy this rule into a claim about every version of standard C: C23 makes a zero-size standard realloc call with a non-NULL pointer undefined. WG14 C23 draft N3096, §7.24.3.7

Decide what NULL means before touching the block

Input Result Original ownership
NULL, 0 NULL No allocation exists
NULL, n>0 Behave as my_malloc(n) New block on success
Live p, 0 Free p, return NULL Ended deliberately
Live p, n>0, success Pointer to resized allocation Continue through returned pointer
Live p, n>0, failure NULL p and its bytes remain valid

A NULL return for positive-size failure and a NULL return for intentional zero-size deallocation have opposite ownership implications. The caller knows which case it requested. Inputs other than NULL must be live payload pointers from this allocator; interior pointers and double frees are outside the contract.

A growth trace with a commit point

Suppose a block has capacity 12 and contains bytes 0..11. A request for 20 bytes follows this order:

  1. Allocate a separate 20-byte destination while the original is still live.
  2. If allocation fails, return NULL immediately; change no old metadata or bytes.
  3. Copy the 12 existing bytes. The additional eight bytes have no initialization guarantee.
  4. Mark the original block free and return the destination.

Allocating first ensures a live old block is not selected for reuse. Copying before freeing prevents another allocation from overwriting the source. The commit point is after the allocation and copy succeed: only then is old ownership released. Copying 20 bytes from a 12-byte source would overread it.

For a shrink from 12 to 5, this exercise keeps the same block and its recorded capacity. The first five bytes are preserved; no new memory is required. Keeping capacity distinct from the requested length explains why a later request for 10 can stay in place. A more advanced allocator can split a remainder, but it must preserve alignment and account for a second header. The exercise does not require splitting or in-place growth through neighboring blocks.

void *my_realloc(void *p, size_t n) {
    if (n == 0) { my_free(p); return NULL; }
    if (p == NULL) return my_malloc(n);
    size_t capacity = get_block_size(p);
    if (n <= capacity) return p;
    void *q = my_malloc(n);
    if (q == NULL) return NULL;
    my_memcpy(q, p, capacity);
    my_free(p);
    return q;
}

my_memcpy copies bytes between valid non-overlapping ranges. It does not provide memmove overlap behavior. get_block_size(NULL) returns 0; for a live pointer it returns payload capacity, excluding the header.

The caller must preserve its only reference

For a positive request, use a temporary:

void *q = my_realloc(p, larger); // larger > 0
if (q != NULL) {
    p = q;
} // otherwise p still owns the original block

Assigning directly to p loses the original pointer if allocation fails. For a zero-size request, explicitly free and set p = NULL; treating that deliberate NULL result as a failed resize would leave a dangling pointer.

Failure safety includes the underlying allocator. Before requesting header + payload + alignment padding, ensure the sum is representable and fits the signed sbrk increment. Otherwise an enormous request can wrap into a small successful allocation, defeating the realloc failure contract. The helper aligns its metadata headers, but this byte-oriented exercise does not implement the complete alignment, threading, or interoperability guarantees of a system malloc.

calloc checks multiplication before allocation

For my_calloc(count, size), either zero argument returns NULL. Otherwise reject size > SIZE_MAX / count, then compute the product and allocate. On success, zero every requested byte, including when the allocator reuses a dirty free block. An allocator cannot rely on fresh operating-system pages to clear recycled memory.

For a toy maximum of 255, count=20, size=13 requires 260 bytes. The guard tests 13 > 255/20 = 12, so it rejects before a wrapped product of 4 could allocate too little. On a real target use SIZE_MAX, not a hard-coded word width. All-bits-zero is the byte contract; it is not a universal representation of every possible C value.

Predict the boundary cases

  1. A positive growth request fails. May the caller still read and free p? Yes; its allocation and contents are unchanged.
  2. my_realloc(p, 0) returns NULL. May the caller still free p? No; this custom API already freed it.
  3. A 40-byte free block is reused for a 16-byte request. What does the simple allocator report as capacity? 40. No split occurred.
  4. calloc reuses that block for four three-byte elements. How many bytes must it clear? 12 requested bytes. Clearing none because the block is recycled breaks its contract.

Practice sequence

Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.

Join Discord