The usual answer to “malloc vs calloc” is that calloc is slower because it zeroes the memory. For a 256 MiB block on the test machine for this article, calloc returned in 0.01 ms. malloc followed by memset took 96 ms. The textbook answer is not wrong for small blocks, but it has the large case backwards, and the reason explains most of what is worth knowing about the two functions.
This guide opens with the comparison table, then covers each function with compiled examples: why malloc’d memory often looks zeroed, the size overflow calloc catches and malloc cannot, what each one actually costs, how to use realloc without leaking, the four heap bugs that matter most, and how C++’s new and delete differ. Every program was compiled and run for this article on Ubuntu 24.04 with glibc 2.39, GCC 13.3 and Clang 18.1.3, C under -std=c11 and -std=c17 and C++ under -std=c++17 and -std=c++20, all with -Wall -Wextra -pedantic. The working examples produce no warnings and run clean under AddressSanitizer and UndefinedBehaviorSanitizer; the deliberately broken ones are shown with the diagnostics they actually produced. All output is captured verbatim.
What Is the Difference Between malloc and calloc?
malloc(size) allocates size bytes and leaves their contents indeterminate. calloc(count, size) allocates space for count objects of size bytes each, sets every byte to zero, and returns NULL if count × size would overflow. Both return a pointer to contiguous memory that must be released with free().
| Aspect | malloc | calloc |
|---|---|---|
| Prototype | void *malloc(size_t size) | void *calloc(size_t count, size_t size) |
| Arguments | Total bytes | Number of elements and size of each |
| Initial contents | Indeterminate: must be written before being read | Every byte zero |
Overflow in count × size | Caller’s problem: the product wraps silently | Checked: returns NULL |
| Cost, small blocks (measured, 64 B) | 8.1 ns per allocate + free | 12.8 ns |
| Cost, large blocks (measured, 256 MiB zeroed) | 96.5 ms including memset | 0.01 ms, with zeroing deferred to first use |
| On failure | Returns NULL | Returns NULL |
| Released with | free() | free() |
| Resized with | realloc() | realloc(); the added bytes are not zeroed |
The measured rows come from the benchmark later in this article and apply to its test machine and C library. The standard’s description of calloc guarantees the zeroing and the overflow check, not the speed.
One detail of “every byte zero” is easy to overlook. All-bits-zero is the integer 0 everywhere, and it is also 0.0 and a null pointer on every mainstream platform. The C standard does not guarantee those last two, so code meant for unusual hardware should still assign 0.0 and NULL explicitly.
malloc, calloc, realloc and free in One Program
The four functions together on one array:
/* basics.c - malloc, calloc, realloc and free on one array */
#include <stdio.h>
#include <stdlib.h>
static void print(const char *label, const int *a, size_t n)
{
printf("%-22s", label);
for (size_t i = 0; i < n; i++)
printf(" %d", a[i]);
printf("\n");
}
int main(void)
{
size_t n = 5;
int *m = malloc(n * sizeof *m); /* contents indeterminate */
int *c = calloc(n, sizeof *c); /* every byte set to zero */
if (m == NULL || c == NULL) {
fprintf(stderr, "out of memory\n");
free(m);
free(c);
return 1;
}
for (size_t i = 0; i < n; i++) /* malloc: write before you read */
m[i] = (int)(i + 1) * 10;
print("malloc, after filling:", m, n);
print("calloc, as returned:", c, n);
int *grown = realloc(m, 2 * n * sizeof *m); /* keep the old pointer */
if (grown == NULL) { /* until this succeeds */
free(m);
free(c);
return 1;
}
m = grown;
for (size_t i = n; i < 2 * n; i++)
m[i] = (int)(i + 1) * 10;
print("realloc to 10:", m, 2 * n);
free(m);
free(c);
return 0;
}
Output:
malloc, after filling: 10 20 30 40 50
calloc, as returned: 0 0 0 0 0
realloc to 10: 10 20 30 40 50 60 70 80 90 100
A few conventions in this code are worth copying:
sizeof *minstead ofsizeof(int). The size follows the pointer’s type, so changinginttolonglater cannot leave a wrong size behind.- No cast on the result. In C,
void *converts to any object pointer type implicitly;(int *)malloc(...)is only needed in C++. - Every result checked for
NULL.free(NULL)does nothing, which is why the error path can free both pointers without checking which one failed. reallocgoes into a temporary. Assigning straight back tomloses the original block ifreallocfails; thereallocsection shows that leak.
The same allocation patterns appear in resizable arrays, and in structures with a flexible array member, where one malloc covers a header and its payload (see structures in C).
“malloc Returns Garbage”, Except When It Doesn’t
Printing a freshly malloc’d array is supposed to show garbage. On this system, printing five ints straight from malloc printed 0 0 0 0 0, exactly like calloc. GCC warned about it ('*<unknown>' may be used uninitialized [-Wmaybe-uninitialized]), and Clang’s MemorySanitizer reported use-of-uninitialized-value. The zeros were luck, not a guarantee, and this program shows where the luck runs out:
/* recycled.c - why malloc'd memory often looks zeroed, until it doesn't */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void show(const char *label, const unsigned char *p, size_t n)
{
printf("%-16s", label);
for (size_t i = 0; i < n; i++)
printf(" %02x", p[i]);
printf("\n");
}
int main(void)
{
enum { N = 32 };
unsigned char *first = malloc(N); /* fresh memory from the OS */
if (first == NULL) return 1;
unsigned char fresh[N];
memcpy(fresh, first, N); /* snapshot before writing */
show("fresh malloc:", fresh, N);
memset(first, 0xAB, N); /* use the block ... */
uintptr_t first_addr = (uintptr_t)first;
free(first); /* ... and give it back */
unsigned char *second = malloc(N); /* same size: often same block */
if (second == NULL) return 1;
unsigned char reused[N];
memcpy(reused, second, N);
show("reused malloc:", reused, N);
printf("same address: %s\n", first_addr == (uintptr_t)second ? "yes" : "no");
unsigned char *third = calloc(1, N);
if (third == NULL) return 1;
show("calloc:", third, N);
free(second);
free(third);
return 0;
}
Output (GCC; Clang’s output has the same pattern):
fresh malloc: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
reused malloc: f3 50 06 65 05 00 00 00 00 00 00 00 00 00 00 00 ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab ab
same address: yes
calloc: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
The first block came from memory the operating system had just handed to the process, and the kernel zeroes pages before giving them to a program. The second request got the same block back, still holding the ab bytes written into it, except for the first 16 bytes, which glibc had used to link the freed block into its free list. The first eight of those bytes changed on every run. None of this is garbage in the sense of random; it is whatever the previous owner left.
This is how the zeroing assumption slips through testing. A program that reads malloc’d memory before writing it works when every allocation is fresh, which is typical of short test runs. It fails later, in a long-running process where blocks are recycled. AddressSanitizer is useful here for a reason beyond its error reports: in an ASan build, malloc fills the first 4 KiB of each new block with 0xbe and does not immediately reuse freed ones. The same program printed be be be … for both allocations, so code that depends on zeroed malloc memory fails in the first test run instead of in production.
The Overflow calloc Catches and malloc Cannot
calloc takes the count and the size separately for a reason. When the element count comes from outside the program, such as a file header or a network message, count * size can exceed SIZE_MAX and wrap around to a small number:
/* overflow.c - n * size can wrap around; calloc checks, malloc cannot */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
/* An element count that might come from a file header or a network message. */
size_t n = (argc > 1) ? strtoull(argv[1], NULL, 0) : ((SIZE_MAX / 4) + 2);
size_t bytes = n * sizeof(int32_t); /* wraps modulo 2^64 */
printf("requested elements: %zu\n", n);
printf("n * 4 computes to: %zu bytes\n", bytes);
int32_t *c = calloc(n, sizeof *c);
printf("calloc(n, 4) -> %s\n", c ? "non-NULL" : "NULL (overflow detected)");
free(c);
int32_t *m = malloc(n * sizeof *m);
if (m != NULL)
printf("malloc(n * 4) -> non-NULL, a %zu-byte block\n", bytes);
else
printf("malloc(n * 4) -> NULL\n");
if (m != NULL) {
m[0] = 1;
m[1] = 2; /* the caller believes n elements exist */
free(m);
}
return 0;
}
Output (GCC and Clang, identical; exit status 0):
requested elements: 4611686018427387905
n * 4 computes to: 4 bytes
calloc(n, 4) -> NULL (overflow detected)
malloc(n * 4) -> non-NULL, a 4-byte block
Four quintillion elements times four bytes wrapped around to 4. calloc noticed and refused. malloc was asked for 4 bytes, returned them, and the program wrote past the end of the block without any visible failure. The ordinary build exited normally. AddressSanitizer, run with allocator_may_return_null=1 so that calloc returns NULL instead of aborting, stops at the write:
ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000014 at pc 0x562699626472 bp 0x7ffcea77f390 sp 0x7ffcea77f380
WRITE of size 4 at 0x502000000014 thread T0
#0 0x562699626471 in main overflow.c:26
0x502000000014 is located 0 bytes after 4-byte region [0x502000000010,0x502000000014)
This pattern, an attacker-controlled count multiplied into a malloc size, is a classic source of heap overflows. With malloc, check the multiplication yourself before calling it: if (n > SIZE_MAX / sizeof *m) fail;. Or use calloc and let it do the check.
How This Was Measured
| Setting | Value |
|---|---|
| Date tested | September 2026 |
| Hardware | One core of an Intel Xeon at 2.10 GHz (cloud VM; nproc reports 1), 4 GB RAM |
| Operating system and C library | Ubuntu 24.04, glibc 2.39, 4 KiB pages |
| Compilers | GCC 13.3 and Clang 18.1.3, -std=c17 -O2 |
| Small-block test | 64-byte blocks, 10,000,000 allocate/write/free cycles |
| Large-block test | 256 MiB, timed separately: allocation (plus memset for malloc), then one write per 4 KiB page |
| Memory measurement | Resident set size from /proc/self/statm, before and after allocation |
| Statistic | Median of 5 runs; each figure checked across 3 separate invocations |
| Guard against the optimizer | memset called through a volatile function pointer (explained below) |
These figures come from one machine, one C library (glibc) and one allocator configuration. There was no CPU pinning or frequency control, and no variance statistics beyond the median and the spread across invocations. Other allocators, such as musl, jemalloc or Windows’ heap, make different choices. Treat the numbers as an illustration of the mechanism, not as constants.
Is calloc Slower Than malloc?
For small blocks, yes; for large blocks that you need zeroed, no. The benchmark program:
/* bench.c - what malloc and calloc cost, small and large */
#define _POSIX_C_SOURCE 199309L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* Called through a volatile pointer so the compiler cannot fold
malloc + memset into a single calloc call. */
static void *(*volatile zero_fill)(void *, int, size_t) = memset;
static double now_ns(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return ts.tv_sec * 1e9 + ts.tv_nsec;
}
static long resident_kib(void) /* current resident set size */
{
long pages_total = 0, pages_res = 0;
FILE *f = fopen("/proc/self/statm", "r");
if (f == NULL) return -1;
if (fscanf(f, "%ld %ld", &pages_total, &pages_res) != 2) pages_res = -1;
fclose(f);
return pages_res * 4; /* 4 KiB pages on this system */
}
static int cmp(const void *a, const void *b)
{
double x = *(const double *)a, y = *(const double *)b;
return (x > y) - (x < y);
}
enum { RUNS = 5 };
int main(void)
{
/* Small blocks: 64 bytes, 10 million allocate/free pairs. */
const size_t small = 64, iters = 10000000;
const char *names[3] = { "malloc", "calloc", "malloc+memset" };
for (int mode = 0; mode < 3; mode++) {
double t[RUNS];
for (int r = 0; r < RUNS; r++) {
double t0 = now_ns();
for (size_t i = 0; i < iters; i++) {
char *p = mode == 1 ? calloc(1, small) : malloc(small);
if (p == NULL) return 1;
if (mode == 2) zero_fill(p, 0, small);
((volatile char *)p)[0] = 1;
free(p);
}
t[r] = (now_ns() - t0) / iters;
}
qsort(t, RUNS, sizeof t[0], cmp);
printf("64 B %-14s %6.1f ns per allocation\n", names[mode], t[RUNS / 2]);
}
/* Large blocks: 256 MiB. Time the allocation, then time writing one
byte per 4 KiB page, and record resident memory in between. */
const size_t big = (size_t)256 << 20;
for (int mode = 1; mode < 3; mode++) {
double ta[RUNS], tw[RUNS];
long rss_after_alloc = 0;
for (int r = 0; r < RUNS; r++) {
long rss0 = resident_kib();
double t0 = now_ns();
char *p = mode == 1 ? calloc(1, big) : malloc(big);
if (p == NULL) return 1;
if (mode == 2) zero_fill(p, 0, big);
double t1 = now_ns();
rss_after_alloc = resident_kib() - rss0;
for (size_t off = 0; off < big; off += 4096)
((volatile char *)p)[off] = 1;
double t2 = now_ns();
ta[r] = (t1 - t0) / 1e6;
tw[r] = (t2 - t1) / 1e6;
free(p);
}
qsort(ta, RUNS, sizeof ta[0], cmp);
qsort(tw, RUNS, sizeof tw[0], cmp);
printf("256 MiB %-14s alloc %7.2f ms, first writes %7.2f ms, RSS after alloc %7ld KiB\n",
names[mode], ta[RUNS / 2], tw[RUNS / 2], rss_after_alloc);
}
return 0;
}
Output (GCC 13.3, -O2):
64 B malloc 8.1 ns per allocation
64 B calloc 12.8 ns per allocation
64 B malloc+memset 9.3 ns per allocation
256 MiB calloc alloc 0.01 ms, first writes 81.16 ms, RSS after alloc 4 KiB
256 MiB malloc+memset alloc 96.49 ms, first writes 0.79 ms, RSS after alloc 262148 KiB
Across three invocations and both compilers, the small-block figures stayed within 7.7–8.9 ns for malloc, 12.1–13.1 ns for calloc and 9.3–10.3 ns for malloc+memset. The large-block calloc first-write time ranged from 78.5 to 94.0 ms, and malloc+memset allocation from 96.5 to 117.3 ms.
Small blocks. calloc cost about 4.7 ns more than malloc per 64-byte allocation. It was also slower than malloc followed by memset, so its extra cost here is more than the zeroing itself.
Large blocks. glibc serves allocations this large with fresh pages from the kernel (via mmap), and those pages are already zero. calloc knows that and skips the zeroing, so it returned in 0.01 ms with essentially nothing resident. The cost did not vanish; it moved. The first write to each page triggers a page fault in which the kernel supplies the zeroed page, and those faults took 81 ms. malloc + memset paid the same page faults inside memset, then zeroed 256 MiB on top: 97 ms in total against 81, with all 256 MiB resident immediately. If some of those pages are never written, as with a sparse table, calloc never pays for them at all.
The optimizer nearly erased this comparison. Written plainly, “malloc then memset(p, 0, n)” was compiled by Clang 18 at -O2 into a single tail call to calloc. GCC 13 does the same once Ubuntu’s default _FORTIFY_SOURCE=3 is switched off (with it on, GCC calls __memset_chk and keeps the two calls). A naive benchmark would have timed calloc twice and reported no difference. That is why bench.c calls memset through a volatile function pointer. The same transformation is also a practical reason not to hand-optimize: writing malloc + memset may give you calloc anyway.
realloc: Growing a Block Without Losing It
realloc(p, new_size) resizes a block. It may extend it in place or allocate a new block, copy the contents, and free the old one. Bytes added by growing are indeterminate, even if the block came from calloc. The one rule that matters: realloc returns NULL on failure and leaves the original block allocated. Assigning the result straight back to the only pointer to the block leaks it:
static void realloc_leak(void)
{
char *buf = malloc(16);
if (buf == NULL) return;
buf = realloc(buf, (size_t)-1 / 2); /* fails: returns NULL ... */
if (buf == NULL) /* ... and the 16 bytes are now lost */
puts("realloc failed");
}
This function is part of bugs.c in the next section. LeakSanitizer reports the lost block:
ERROR: LeakSanitizer: detected memory leaks
Direct leak of 16 byte(s) in 1 object(s) allocated from:
#1 0x5603a921e43e in realloc_leak bugs.c:31
The fix is the pattern in basics.c above: assign to a temporary, and replace the original pointer only after checking for NULL.
Avoid realloc(p, 0). On glibc it returned NULL and freed the block, but C17 left the behavior implementation-defined, and implementations disagreed about whether the block was freed. C23 made it undefined behavior. To release memory, call free.
Four Heap Bugs and What Catches Them
/* bugs.c - four heap bugs; pick one with a command-line argument */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void leak(void)
{
char *name = malloc(32);
if (name == NULL) return;
strcpy(name, "never freed");
} /* pointer lost, block never freed */
static void double_free(void)
{
int *p = malloc(sizeof *p);
free(p);
free(p); /* second free of the same block */
}
static void use_after_free(void)
{
int *p = malloc(sizeof *p);
if (p == NULL) return;
*p = 42;
free(p);
printf("%d\n", *p); /* read after free */
}
static void realloc_leak(void)
{
char *buf = malloc(16);
if (buf == NULL) return;
buf = realloc(buf, (size_t)-1 / 2); /* fails: returns NULL ... */
if (buf == NULL) /* ... and the 16 bytes are now lost */
puts("realloc failed");
}
int main(int argc, char **argv)
{
const char *which = argc > 1 ? argv[1] : "";
if (strcmp(which, "leak") == 0) leak();
else if (strcmp(which, "double") == 0) double_free();
else if (strcmp(which, "uaf") == 0) use_after_free();
else if (strcmp(which, "realloc") == 0) realloc_leak();
else puts("usage: bugs leak|double|uaf|realloc");
return 0;
}
What each build did:
| Bug | Compiler warning (-Wall) | Plain build, -O0 | Plain build, -O2 | AddressSanitizer |
|---|---|---|---|---|
| Leak | None | Exit 0, nothing reported | Exit 0 | Direct leak of 32 byte(s) |
| Double free | GCC: pointer 'p' used after 'free' [-Wuse-after-free]; Clang: none | Aborted: free(): double free detected in tcache 2 | Exit 0: GCC and Clang removed the allocation entirely | attempting double-free |
| Use after free | GCC: -Wuse-after-free; Clang: none | Printed a different large number on every run | Same | heap-use-after-free … READ of size 4 |
realloc leak | None | Printed realloc failed, exit 0 | GCC: same. Clang: printed nothing; it removed the allocation and assumed realloc succeeded | Direct leak of 16 byte(s) |
Two results stand out:
- Optimization removed bugs from the binary instead of exposing them. At
-O2both compilers deleteddouble_free’smallocand bothfreecalls, because the block was never used, so the-O2binary ran cleanly while the-O0binary aborted. Clang went further withrealloc_leak: it removed the allocation and therealloccall and treated the reallocation as successful, so the failure branch never ran. Compilers are allowed to elide allocations whose results are not otherwise used, which means an optimized test run can say nothing about these code paths. - Leaks produce no symptom in an ordinary build. The two leak rows are invisible without a leak checker.
A program can pass every test at -O2 and still contain all four bugs. Build and run tests with -fsanitize=address (which includes the leak checker on Linux) as a matter of routine.
C++: new, delete and Why You Rarely Need Them
C++ can call malloc and free, but they allocate raw bytes and never run constructors or destructors. C++’s own operators do both:
// cxx.cpp - the same allocations in C++
#include <cstdio>
#include <memory>
#include <new>
#include <string>
#include <vector>
int main()
{
int* raw = new int[5](); // () value-initializes: all zero
std::printf("new int[5](): %d %d %d %d %d\n", raw[0], raw[1], raw[2], raw[3], raw[4]);
delete[] raw; // new[] pairs with delete[]
auto owned = std::make_unique<int[]>(5); // also zeroed; freed automatically
std::printf("make_unique: %d %d %d %d %d\n", owned[0], owned[1], owned[2], owned[3], owned[4]);
std::vector<std::string> names(3, "n/a"); // constructors run for every element
names.resize(5); // grows like realloc, but copies safely
std::printf("vector size %zu, names[4] = \"%s\"\n", names.size(), names[4].c_str());
try {
std::size_t huge = static_cast<std::size_t>(-1) / 2;
char* p = new char[huge]; // failure throws instead of returning null
delete[] p;
} catch (const std::bad_alloc& e) {
std::printf("new failed: %s\n", e.what());
}
int* maybe = new (std::nothrow) int[1000]; // opt back in to a null result
std::printf("nothrow new: %s\n", maybe ? "non-null" : "null");
delete[] maybe;
}
Output (g++ and clang++, identical):
new int[5](): 0 0 0 0 0
make_unique: 0 0 0 0 0
vector size 5, names[4] = ""
new failed: std::bad_alloc
nothrow new: non-null
The std::bad_alloc line is the one part of these examples that behaves differently under AddressSanitizer: there, the oversized new aborts with an out-of-memory report instead of throwing, so the sanitizer run used a small allocation in its place.
| Aspect | C: malloc / calloc | C++: new / delete |
|---|---|---|
| Size argument | Bytes | Number of objects; the type supplies the size |
| Constructors and destructors | Not called | Called |
| Zeroing | calloc only | new T[n]() value-initializes; new T[n] does not |
| Failure | Returns NULL | Throws std::bad_alloc; new (std::nothrow) returns null |
| Resizing | realloc | No equivalent; std::vector grows and moves elements correctly |
| Release | free | delete for new, delete[] for new[] |
Mixing the two families is undefined behavior. Neither mistake below crashed in an ordinary build:
int* a = new int[4];
delete a; // should be delete[]
int* b = static_cast<int*>(std::malloc(sizeof(int)));
delete b; // should be std::free
g++ warned about both lines (called on pointer returned from a mismatched allocation function [-Wmismatched-new-delete]). clang++ warned only about the first ('delete' applied to a pointer that was allocated with 'new[]'; did you mean 'delete[]'?). The plain build exited normally, and AddressSanitizer stopped at the first mismatch: alloc-dealloc-mismatch (operator new [] vs operator delete).
In modern C++, the practical answer is to write none of these calls. Use std::vector for arrays, std::string for text, and std::make_unique for single owned objects, and let destructors free the memory.
Which One Should You Use?
| Situation | Use | Why |
|---|---|---|
| You will write every byte before reading any | malloc | Cheapest for small blocks; zeroing would be wasted |
| You need the memory zeroed | calloc | Correct, and for large blocks faster than malloc + memset in the measured case |
| The element count comes from input | calloc, or malloc with an explicit overflow check | calloc rejects a wrapped count × size |
| A large table that may be sparsely used | calloc | Untouched pages were never made resident in the measured case |
| Growing an existing block | realloc, into a temporary pointer | Avoids leaking the block on failure |
| C++ code | std::vector, std::make_unique | Constructors, destructors and ownership handled for you |
Key Takeaways
mallocleaves memory indeterminate;calloczeroes it and checkscount × sizefor overflow. Both are released withfree.- Fresh
mallocmemory often reads as zero, and recycled memory does not. A reused block still held the previous owner’sabbytes. Never rely on the zeros. calloccatches the multiplication overflow thatmalloccannot. A wrapped size gavemalloca 4-byte block and a silent heap overflow.callocis slower for small blocks and faster for large zeroed ones. 12.8 vs 8.1 ns at 64 bytes; 0.01 ms vs 96.5 ms to obtain 256 MiB of zeroed memory, on the test machine.- Assign
realloc’s result to a temporary. A failedreallocleaves the original block allocated, and overwriting the only pointer leaks it. - Heap bugs hide in ordinary builds. An optimized double free disappeared entirely, and leaks produced no symptom; AddressSanitizer caught all four bugs.
- In C++, prefer containers and smart pointers to
newanddelete, and never mixmalloc/freewithnew/delete.
Frequently Asked Questions
Conclusion
The choice between malloc and calloc is less about speed than about promises. calloc promises zeroed memory and an overflow check on its arguments, and on a modern system it can keep the first promise almost for free, because the kernel was going to zero fresh pages anyway. malloc promises only bytes, and the bugs around it come from code that assumes more than that: zeros that happened to be there, a multiplication that wrapped, a realloc that failed.
The heap bugs in this article share one property: none announced itself in an ordinary build. That makes the sanitizers, rather than any particular allocation function, the most valuable habit here. The rest of the C programming tutorials cover the pointers, arrays and data structures that sit on top of these four functions.



