C / DYNAMIC MEMORY
calloc for zeroed arrays and multiplication overflow
Allocate zero-filled arrays with calloc and understand why letting the library multiply count by element size stops a silent size_t wraparound.
What you will learn
- Write calloc(n, sizeof *p) so the library does the multiplication and its overflow check
- Read zeroed memory directly: counters start at 0, so the init loop disappears
- Guard your own products with n > SIZE_MAX / size before calling malloc or realloc
- Know that all-bits-zero is integer 0, not a guaranteed null pointer or 0.0
Understanding calloc for zeroed arrays and multiplication overflow
calloc takes the shape of the array apart: calloc(n, sizeof *p) says "n objects, this many bytes each" instead of handing malloc one byte count you computed yourself. The library performs the multiplication, and that is the whole point, because it is the only party in a position to refuse a product that cannot describe a real array. It also guarantees the returned bytes are all zero, which malloc never promises; a malloc block holds whatever the allocator last left there, and reading an element before writing it gives you an indeterminate value.
size_t is unsigned, so n * sizeof *p never overflows in the trapping sense. It wraps modulo SIZE_MAX + 1, quietly and legally, with no signal and usually no warning. Choose n = SIZE_MAX / sizeof(int) + 1 and the product is exactly 0; malloc(0) is a legal call that glibc answers with a unique non-null pointer, so the if (p == NULL) line you were counting on reports success and the very first p[0] = ... write is already past the end of the block. calloc cannot be fooled this way because it is defined as allocating space for an array of nmemb objects of size size, and when no such size is representable glibc, musl and macOS return NULL with errno set to ENOMEM.
"Zeroed" means all bits zero, not "each element set to its natural empty value". For every integer type all-bits-zero is the value 0, so counts, lengths and flags are ready to use. Pointers and floating-point members are the fine print: on any target you are realistically compiling for, all-bits-zero is a null pointer and +0.0, but the C standard does not require either, so portable code still assigns NULL explicitly where it matters. Size also changes the cost: for a large request the allocator typically takes fresh pages from the kernel that are already zero and are faulted in lazily, so calloc can beat malloc followed by memset, while a small block recycled from the free list simply gets memset and there is no saving.
<errno.h>
<stdint.h>
<stdio.h>
<stdlib.h>
int main(void)
{
size_t n = 6;
int *counts = calloc(n, sizeof *counts);
if (counts == NULL) {
perror("calloc");
return 1;
}
printf("fresh:");
for (size_t i = 0; i < n; i++)
printf(" %d", counts[i]);
printf("\n");
counts[2] = 7;
counts[5] = -1;
printf("used:");
for (size_t i = 0; i < n; i++)
printf(" %d", counts[i]);
printf("\n");
free(counts);
/* Pretend this count was read from a file. */
size_t bad = SIZE_MAX / sizeof(int) + 1;
printf("bad * sizeof(int) = %zu\n", bad * sizeof(int));
errno = 0;
int *rejected = calloc(bad, sizeof *rejected);
printf("calloc gave %s, errno is ENOMEM: %s\n",
rejected == NULL ? "NULL" : "a block",
errno == ENOMEM ? "yes" : "no");
free(rejected);
return 0;
}
calloc keeps the count-times-element-size multiplication inside the allocator, the only place it can be checked and refused instead of silently wrapping into a too-small block.
Worked examples
Counters that start at zero
Zero-initialised memory lets a frequency table be incremented immediately, with no setup loop.
<stdio.h>
<stdlib.h>
int main(void)
{
const char *text = "banana bread";
unsigned *freq = calloc(26, sizeof *freq);
if (freq == NULL)
return 1;
for (const char *p = text; *p != '\0'; p++)
if (*p >= 'a' && *p <= 'z')
freq[*p - 'a']++;
for (int i = 0; i < 26; i++)
if (freq[i] != 0)
printf("%c=%u\n", 'a' + i, freq[i]);
free(freq);
return 0;
}
Example explained
Line 1calloc(26, sizeof *freq) returns 26 unsigned counters already holding 0, so the loop can start with ++.
Line 2With malloc, freq[*p - 'a']++ would read an indeterminate value first and the printed counts would be junk.
Line 3sizeof *freq is derived from the pointer, so changing freq to size_t * needs no other edit.
Line 4The element count and the element size stay separate arguments; calloc is the one multiplying them.
The same check by hand
Reproduces calloc's overflow test with division, for the cases where the allocation must go through malloc.
<stdint.h>
<stdio.h>
<stdlib.h>
<string.h>
static void *zeroed_array(size_t n, size_t size)
{
if (size != 0 && n > SIZE_MAX / size)
return NULL; /* n * size would wrap */
void *p = malloc(n * size);
if (p != NULL)
memset(p, 0, n * size);
return p;
}
int main(void)
{
double *v = zeroed_array(4, sizeof *v);
if (v == NULL)
return 1;
printf("v[3] = %.1f\n", v[3]);
free(v);
void *huge = zeroed_array(SIZE_MAX / 2, 3);
printf("SIZE_MAX/2 by 3: %s\n", huge == NULL ? "refused" : "allocated");
free(huge);
return 0;
}
Example explained
Line 1n > SIZE_MAX / size asks whether the product fits without ever computing the product that would wrap.
Line 2The size != 0 test exists only to keep that division from dividing by zero.
Line 3SIZE_MAX / 2 exceeds SIZE_MAX / 3, so the request is refused before malloc is called at all.
Line 4v[3] prints 0.0 because all-bits-zero is +0.0 in IEEE-754, the same platform coincidence calloc leans on.
Important notes
calloc(0, size) and calloc(n, 0) may return either NULL or a unique pointer you must still free, so NULL from a zero-sized request is not necessarily a failure.
glibc and the BSDs offer reallocarray(p, n, size), which applies calloc's overflow check while growing a block; it is an extension, not standard C.
Common mistakes
Collapsing the arguments into calloc(n * sizeof *p, 1): the multiplication is back in your code, the wrap happens before calloc sees it, and you get a block far smaller than n elements.
Trusting if (p == NULL) after malloc(n * size) when n came from input: once the product wraps to 0, glibc returns a real non-null pointer, the check passes, and every element write is a heap buffer overflow.
Reading "zeroed" as "initialised": a calloc'd struct whose int fd field is 0 looks like a valid stdin, and a zeroed enum silently means whichever enumerator equals 0, so later code acts on a field nobody ever set.
Try it yourself
Change, predict, then run
Allocate 10 ints with calloc, print their sum without writing to them, and confirm it is 0. Then call calloc(SIZE_MAX / sizeof(int) + 1, sizeof(int)) and print whether it returned NULL, and replace that call with malloc of the same wrapped product to see the dangerous request succeed.
Open the C workspaceCheck your understanding
A count n is read from a file and happens to equal SIZE_MAX / 4 + 1 on a machine where int is 4 bytes. Why is int *p = calloc(n, sizeof *p); safer than int *p = malloc(n * sizeof *p);?
- n * sizeof *p wraps to 0, malloc gets a legal zero-byte request and glibc hands back a non-null pointer, so the NULL check passes and writing p[0] already overflows the heap, while calloc sees that no array of that shape is representable and returns NULL
- Multiplying size_t values beyond their maximum is undefined behaviour, so the malloc version may crash unpredictably, while calloc performs the multiplication in wider arithmetic
- calloc zeroes the block, so the out-of-range writes land in bytes that were already cleared and cannot corrupt anything
- malloc refuses any request above SIZE_MAX / 2 bytes, while calloc splits a large request into several smaller blocks
Show answer
Unsigned arithmetic is defined to wrap modulo SIZE_MAX + 1, which is precisely why the bug is silent: the undefined-behaviour option is wrong because nothing traps and no diagnostic is required. The wrapped value 0 is an acceptable argument to malloc and glibc returns a unique non-null pointer for it, so the usual NULL check gives false confidence, whereas calloc is specified to allocate space for n objects of the given size and returns NULL with errno set to ENOMEM when that product cannot be represented. Zeroing is irrelevant to the overflow: the writes go past the end of whatever small block was actually handed out.