realloc and calloc
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesIn this lesson
Lesson
5 min readResize 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:
- Allocate a separate 20-byte destination while the original is still live.
- If allocation fails, return NULL immediately; change no old metadata or bytes.
- Copy the 12 existing bytes. The additional eight bytes have no initialization guarantee.
- 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
- A positive growth request fails. May the caller still read and free
p? Yes; its allocation and contents are unchanged. my_realloc(p, 0)returns NULL. May the caller still freep? No; this custom API already freed it.- A 40-byte free block is reused for a 16-byte request. What does the simple allocator report as capacity? 40. No split occurred.
callocreuses 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.
- Step 1
Implementing realloc
- Step 2
Implementing calloc
- Step 3
Realloc and Calloc Quiz
Test understanding of realloc and calloc