C Part I · Module 9 · Memory and Object Lifetime
calloc and realloc
The previous chapter left two loose ends. A fresh block is not zeroed, so an allocation that should start from zero needs a loop immediately after it. And a block's size is fixed when you ask for it, so a program that discovers it needs more has nothing to say. The standard library closes both gaps with one function each. The first is straightforward and has a hidden advantage most people never learn about — it is not the zeroing. The second is the single most dangerous function in this part of the library, because the obvious way to call it is wrong, compiles without a murmur, works perfectly in every test you will run, and destroys your data on the one day memory runs short.
Intermediate20 min readCPart IFundamentalsMemory
Part I · Module 9 · Chapter 9.7 · calloc and realloc
1. The Family
#include <stdlib.h>
void *malloc (size_t size);
void *calloc (size_t count, size_t each);
void *realloc(void *pointer, size_t new_size);
void free (void *pointer);Four functions, and every one of them is now familiar in shape: each returns void * or takes it, each reports failure with NULL, and all four come from <stdlib.h>.
| Function | What it does | Returns NULL when |
|---|---|---|
malloc | a block of size bytes, contents indeterminate | the request cannot be met |
calloc | a block of count × each bytes, all bytes zero | the request cannot be met, or the product overflows |
realloc | resizes an existing block, preserving contents | the request cannot be met — and the old block survives |
free | ends a block's lifetime | — |
The two phrases in bold are what this chapter is about. The second one is the one that will cost you data if you only half-learn it.
2. calloc: Allocate and Zero
int *counters = calloc(8, sizeof *counters);Note the two arguments. calloc takes a count and a size per item, where malloc takes a single byte count. This is not arbitrary, as §3 will show.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
size_t n = 6;
int *zeroed = calloc(n, sizeof *zeroed);
if (zeroed == NULL) {
return 1;
}
printf("calloc contents:");
for (size_t i = 0; i < n; i++) {
printf(" %d", zeroed[i]);
}
printf("\n");
free(zeroed);
return 0;
}Output:
calloc contents: 0 0 0 0 0 0Every byte of the block is set to zero. For the integer types that is the value 0, which is exactly what a counter, a running total, or an accumulator wants to start from.
Two cautions on "zeroed", both of which matter later.
All-bytes-zero is not the same as "zero" for every type. For integers and characters it is. For pointers, a null pointer is usually all-bits-zero but Chapter 7.4 §2 established that C does not guarantee it, so calloc is not a portable way to produce an array of null pointers. For floating-point types, all-bits-zero happens to be 0.0 on every format you will meet in practice, but the standard does not promise it either.
calloc zeroes the bytes, not your invariants. If the first thing your code does is overwrite every element anyway, calloc buys you nothing but a pass over the memory.
Use
callocwhen zero is the value you actually want to start from. Usemallocwhen you are about to fill every element. Choosing by habit rather than by intent is how a program ends up paying for a zeroing it discards one line later.
3. The Real Reason to Prefer calloc
Here is the argument that textbooks skip. malloc takes a single byte count, so a request for an array is written as a multiplication — and size_t arithmetic wraps, as Chapter 2.11 established. calloc takes the two factors separately, which means it can check.
The program below uses SIZE_MAX, which is simply the largest value a size_t can hold. It comes from <stdint.h>, the header Chapter 2.12 used for the fixed-width types, and it is the cleanest way to construct a multiplication that is certain to overflow.
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main(void)
{
size_t count = SIZE_MAX / 2 + 1; /* count * 4 overflows size_t */
size_t each = 4;
printf("count = %zu\n", count);
printf("count * each = %zu (wrapped)\n", count * each);
void *m = malloc(count * each);
printf("malloc(count * each) non-null ? %d\n", m != NULL);
free(m);
void *c = calloc(count, each);
printf("calloc(count, each) non-null ? %d\n", c != NULL);
free(c);
return 0;
}Output:
count = 9223372036854775808
count * each = 0 (wrapped)
malloc(count * each) non-null ? 1
calloc(count, each) non-null ? 0Read those last two lines carefully, because they describe a security hole.
The multiplication wrapped to zero. malloc was therefore asked for zero bytes, and it succeeded — it returned a non-null pointer. A program that then writes count elements through that pointer is writing billions of items into a block of no size, and nothing in the language, the compiler, or the warning flags will tell it so. The code looked like a perfectly ordinary allocation.
calloc received the factors unmultiplied, noticed that the product could not be represented, and returned NULL — which the mandatory check from Chapter 9.6 §3 turns into a clean, early, correct failure.
When the element count comes from outside your program — a file header, a network field, a user-supplied length —
callocis the safer call, because the overflow check is the library's job rather than yours. Withmallocyou must do the check yourself before the multiply, and the check is easy to forget precisely because the multiply looks harmless.
This is also why the two-argument signature exists at all. It has nothing to do with zeroing.
4. realloc: Changing a Block's Size
void *realloc(void *pointer, size_t new_size);realloc takes an existing block and produces one of new_size bytes with the contents preserved as far as they fit. Growing keeps everything; shrinking keeps the leading part and discards the rest.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
size_t n = 6;
int *values = malloc(n * sizeof *values);
if (values == NULL) {
return 1;
}
for (size_t i = 0; i < n; i++) {
values[i] = (int)i + 1;
}
int *bigger = realloc(values, 12 * sizeof *bigger);
if (bigger == NULL) {
free(values);
return 1;
}
values = NULL; /* bigger owns the block now */
printf("after growing, first six preserved:");
for (size_t i = 0; i < 6; i++) {
printf(" %d", bigger[i]);
}
printf("\n");
free(bigger);
return 0;
}Output:
after growing, first six preserved: 1 2 3 4 5 6Two conveniences are worth knowing.
realloc(NULL, size) behaves exactly like malloc(size). Verified. This lets a growing-buffer routine start from a null pointer and use one code path for the first allocation and every subsequent growth.
Shrinking works and keeps the leading elements. Verified: a block of eight holding 0 11 22 33 ... shrunk to three still reads 0 11 22.
One thing is not convenient. realloc(pointer, 0) is a trap in slow motion: in C17 it is implementation-defined, and on this implementation it returned a non-null pointer rather than freeing the block. C23 made it undefined behaviour outright. Never use it to release memory — that is what free is for.
5. The Block May Move
realloc is allowed to satisfy the request by placing the block somewhere else entirely and copying the contents across. It usually does when growing by a lot.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p = malloc(4 * sizeof *p);
if (p == NULL) {
return 1;
}
printf("before %p\n", (void *)p);
int *q = realloc(p, 400000 * sizeof *q);
if (q == NULL) {
free(p);
return 1;
}
printf("after %p moved ? %d\n", (void *)q, (void *)q != (void *)p);
free(q);
return 0;
}One run:
before 0x104b45a00
after 0x904800000 moved ? 1The addresses differ, so the block moved. Whether it moves is unspecified — the same program may grow in place on another run, another input size, or another implementation. Never write code whose correctness depends on the answer.
The consequence is the important part: when the block moves, every other pointer into it becomes invalid, including the one you passed in. If some other variable, structure field, or array element was holding an address inside that block, it is now holding an address into released storage. Chapter 9.11 enumerates this as one of the ways a pointer goes bad.
6. The Failure That Loses Your Buffer
This is the line almost everyone writes the first time:
p = realloc(p, new_size); /* WRONG */It is wrong, and the reason is in Figure 1's bottom row.
When realloc cannot satisfy the request it returns NULL and leaves the original block exactly as it was — allocated, intact, and still your responsibility. That behaviour is deliberate and useful: it means a failed growth costs you nothing.
But the assignment above has already thrown away the only thing that could reach it. p is now NULL. The block still exists. Nothing in the program knows where it is. It cannot be used and it cannot be freed.
That the original survives is not a claim to take on trust:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p = malloc(4 * sizeof *p);
if (p == NULL) {
return 1;
}
p[0] = 7;
void *attempt = realloc(p, (size_t)-1); /* cannot succeed */
printf("realloc returned NULL ? %d\n", attempt == NULL);
printf("original still valid, p[0] = %d\n", p[0]);
free(p);
return 0;
}Output:
realloc returned NULL ? 1
original still valid, p[0] = 7The result went into a separate variable, so p still reaches the block, p[0] still reads 7, and the free at the end still works. Change void *attempt = to p = and all three of those sentences stop being true.
7. The Correct Shape
Put the result in a new variable, check it, and only then commit:
int *attempt = realloc(buffer, new_count * sizeof *attempt);
if (attempt == NULL) {
/* buffer is untouched, still valid, still ours to free */
return -1;
}
buffer = attempt; /* commit only on success */Three lines, and the ordering is the whole point: nothing overwrites buffer until the new block is known to exist. This is the same discipline as Chapter 9.6 §3's early return, applied to a call that can fail without failing cleanly.
Wrapped into a function, it becomes reusable:
#include <stdio.h>
#include <stdlib.h>
/* returns 1 on success; on failure returns 0 and leaves *buf untouched */
static int grow(int **buf, size_t new_count)
{
int *attempt = realloc(*buf, new_count * sizeof **buf);
if (attempt == NULL) {
return 0; /* *buf is still valid and still owned */
}
*buf = attempt;
return 1;
}
int main(void)
{
size_t cap = 2, used = 0;
int *v = malloc(cap * sizeof *v);
if (v == NULL) {
return 1;
}
for (int x = 1; x <= 9; x++) {
if (used == cap) {
if (!grow(&v, cap * 2)) {
printf("grow failed\n");
free(v);
return 1;
}
cap = cap * 2;
printf("capacity now %zu\n", cap);
}
v[used++] = x * x;
}
printf("%zu of %zu used:", used, cap);
for (size_t i = 0; i < used; i++) {
printf(" %d", v[i]);
}
printf("\n");
if (!grow(&v, (size_t)-1 / sizeof *v - 1)) {
printf("huge grow refused, v still holds:");
for (size_t i = 0; i < used; i++) {
printf(" %d", v[i]);
}
printf("\n");
}
free(v);
return 0;
}Output:
capacity now 4
capacity now 8
capacity now 16
9 of 16 used: 1 4 9 16 25 36 49 64 81
huge grow refused, v still holds: 1 4 9 16 25 36 49 64 81Wait — look again at the final two lines. The refused growth did not corrupt anything, did not crash, and did not lose a single element. The buffer is intact and the subsequent free releases it correctly. That is what a correct failure looks like, and it is only possible because grow never touched *buf until it had something to put there.
The int **buf parameter is Chapter 7.10's pointer-to-pointer, used for exactly the reason that chapter gave: the function must change the caller's pointer variable, so it needs that variable's address. The doubling of capacity is a standard growth strategy — it keeps the total number of realloc calls proportional to the number of items rather than to the number of insertions.
8. Choosing Between Them
| You want | Use |
|---|---|
| a block you are about to fill completely | malloc |
| a block that should start as all zeroes | calloc |
| a block whose element count came from outside the program | calloc — for the overflow check |
| to change the size of a block you already have | realloc, into a new variable |
| the first allocation of a buffer that will grow | malloc, or realloc(NULL, size) |
| to release a block | free — never realloc(p, 0) |
And one rule that outranks the table: whichever you call, check the result before using it, and free the block exactly once no matter which function produced it. free does not care which of the three allocated the block.
9. What You Should Be Able to Say Now
calloc(count, each)allocatescount × eachbytes with every byte zero.- All-bytes-zero means
0for integers but is not guaranteed to be a null pointer or0.0. calloc's two arguments exist so it can detect the overflow thatmalloc(count * each)cannot. Verified: the wrapped multiply mademallocallocate zero bytes and report success, whilecallocreturnedNULL.realloc(p, n)resizes a block and preserves the contents as far as they fit.realloc(NULL, n)ismalloc(n); shrinking keeps the leading elements.realloc(p, 0)is implementation-defined in C17 and undefined in C23 — never use it to free.- The block may move, which is unspecified, and any other pointer into it becomes invalid — Chapter 9.11.
- On failure
reallocreturnsNULLand the original block survives intact, verified. - Therefore
p = realloc(p, n)loses the buffer on failure: the block exists and nothing can reach it. - The correct shape is new variable, check, then commit — and a line that is only wrong on the failure path is a line your tests never run.
10. Exercises
Exercise 1 — choose the function. For each, say which of malloc, calloc, or realloc is right and why.
- an array of 64 running totals, all starting at zero
- a buffer about to be filled from a file, completely
- an array whose element count was read from a file header
- a list that has just run out of room
Exercise 2 — spot the loss. Three of these four lose the block when the allocation is refused. Identify them.
a = realloc(a, n);
tmp = realloc(b, n); if (tmp) b = tmp;
c = realloc(c, n); if (c == NULL) return -1;
d = realloc(d, n); if (d == NULL) { free(d); return -1; }Exercise 3 — classify six claims. ISO C guarantee, implementation-defined, unspecified, undefined, or false?
callocsets every byte of the block to zerocallocgives you an array of null pointersreallocreturns the same address when shrinkingreallocreturnsNULLwithout freeing the original on failurerealloc(p, 0)frees the block in C23freeworks on a block that came fromcalloc
Exercise 4 — write the overflow check. You must use malloc, not calloc, and count arrives from untrusted input. Write the guard that goes before the multiplication, using SIZE_MAX from <stdint.h>. What comparison makes the multiply safe?
Exercise 5 — predict the output. Then compile it and compare.
int *p = calloc(3, sizeof *p);
if (p == NULL) return 1;
printf("%d %d %d\n", p[0], p[1], p[2]);
p[1] = 5;
int *q = realloc(p, 5 * sizeof *q);
if (q == NULL) { free(p); return 1; }
printf("%d %d\n", q[0], q[1]);
free(q);Which element of q is it a mistake to print, and why?
Exercise 6 — repair the function. Four faults. Name all four.
char *extend(char *s, int n)
{
s = realloc(s, n);
if (s == NULL) { free(s); }
return s;
}Exercise 7 — the stale interior pointer. A program keeps int *buf and also int *cursor = &buf[3]. It then grows buf correctly, using the new-variable form. Say what is wrong with cursor afterwards, whether it is wrong in all three of Figure 1's outcomes, and how you would restructure the code so the question cannot arise.
Exercise 8 — explain the test gap. In two or three sentences, explain why p = realloc(p, n) passes every test on a developer machine, and name one other kind of bug that hides for the same reason.
11. Summary
calloc(count, each)allocates and zeroes; its two arguments also let it detect a size overflow thatmalloccannot.- Measured: a wrapping multiply made
mallocallocate zero bytes and succeed;callocreturnedNULL. - All-bytes-zero is
0for integers, but not guaranteed for pointers or floating point. reallocresizes and preserves contents;realloc(NULL, n)ismalloc(n);realloc(p, 0)is never how you free.- The block may move — unspecified — invalidating every other pointer into it.
- On failure
reallocreturnsNULLand the original block survives. - So
p = realloc(p, n)is a data-loss bug: new variable, check, then commit. - A line that is only wrong on the failure path never runs in your tests.
Every rule in this chapter and the last one has been about a single block held by a single pointer in a single function. Real programs are not shaped like that. A buffer gets allocated in one function, filled by a second, passed to a third, stored in a structure by a fourth, and released — supposedly — somewhere far away. All three functions you now know are correct in isolation; none of them tells you who is supposed to call free. That question has no syntax at all in C. It is answered only by a convention you choose and document, and the next chapter is about how to choose one that your readers cannot get wrong.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
