malloc vs calloc in C: Differences, realloc, free and the Bugs That Matter

The difference between malloc and calloc in C, with measured costs, realloc and free, four heap bugs caught by sanitizers, and C++ new and delete.

Two open boxes, one empty and clean and one still holding its previous contents, illustrating the difference between calloc and malloc in C

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().

Aspectmalloccalloc
Prototypevoid *malloc(size_t size)void *calloc(size_t count, size_t size)
ArgumentsTotal bytesNumber of elements and size of each
Initial contentsIndeterminate: must be written before being readEvery byte zero
Overflow in count × sizeCaller’s problem: the product wraps silentlyChecked: returns NULL
Cost, small blocks (measured, 64 B)8.1 ns per allocate + free12.8 ns
Cost, large blocks (measured, 256 MiB zeroed)96.5 ms including memset0.01 ms, with zeroing deferred to first use
On failureReturns NULLReturns NULL
Released withfree()free()
Resized withrealloc()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 *m instead of sizeof(int). The size follows the pointer’s type, so changing int to long later 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.
  • realloc goes into a temporary. Assigning straight back to m loses the original block if realloc fails; the realloc section 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

SettingValue
Date testedSeptember 2026
HardwareOne core of an Intel Xeon at 2.10 GHz (cloud VM; nproc reports 1), 4 GB RAM
Operating system and C libraryUbuntu 24.04, glibc 2.39, 4 KiB pages
CompilersGCC 13.3 and Clang 18.1.3, -std=c17 -O2
Small-block test64-byte blocks, 10,000,000 allocate/write/free cycles
Large-block test256 MiB, timed separately: allocation (plus memset for malloc), then one write per 4 KiB page
Memory measurementResident set size from /proc/self/statm, before and after allocation
StatisticMedian of 5 runs; each figure checked across 3 separate invocations
Guard against the optimizermemset 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.

Where calloc and malloc spend their time Median of 5 runs, GCC 13.3 -O2, glibc 2.39, one core of a 2.10 GHz Xeon 256 MiB of zeroed memory, then one write per 4 KiB page RSS after allocation calloc 81.2 ms 4 KiB malloc + memset 97.3 ms 262,148 KiB 0 ms 25 ms 50 ms 75 ms 100 ms allocation and zeroing first write to each page (page faults) calloc got its zeroed pages from the kernel without touching them, so the cost moved to first use. 64-byte blocks: one allocate and free malloc 8.1 ns malloc + memset 9.3 ns calloc 12.8 ns 0 ns 4 ns 8 ns 12 ns For small blocks the fixed cost dominates: calloc was slower than malloc followed by memset.
Zeroing is not what makes large allocations expensive; touching new pages is. Both approaches pay for page faults, but only malloc followed by memset also writes 256 MiB of zeros and makes all of it resident up front. For tiny blocks the picture flips, because per-call overhead outweighs the few bytes being cleared.

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:

BugCompiler warning (-Wall)Plain build, -O0Plain build, -O2AddressSanitizer
LeakNoneExit 0, nothing reportedExit 0Direct leak of 32 byte(s)
Double freeGCC: pointer 'p' used after 'free' [-Wuse-after-free]; Clang: noneAborted: free(): double free detected in tcache 2Exit 0: GCC and Clang removed the allocation entirelyattempting double-free
Use after freeGCC: -Wuse-after-free; Clang: nonePrinted a different large number on every runSameheap-use-after-free … READ of size 4
realloc leakNonePrinted realloc failed, exit 0GCC: same. Clang: printed nothing; it removed the allocation and assumed realloc succeededDirect leak of 16 byte(s)

Two results stand out:

  • Optimization removed bugs from the binary instead of exposing them. At -O2 both compilers deleted double_free’s malloc and both free calls, because the block was never used, so the -O2 binary ran cleanly while the -O0 binary aborted. Clang went further with realloc_leak: it removed the allocation and the realloc call 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.

AspectC: malloc / callocC++: new / delete
Size argumentBytesNumber of objects; the type supplies the size
Constructors and destructorsNot calledCalled
Zeroingcalloc onlynew T[n]() value-initializes; new T[n] does not
FailureReturns NULLThrows std::bad_alloc; new (std::nothrow) returns null
ResizingreallocNo equivalent; std::vector grows and moves elements correctly
Releasefreedelete 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?

SituationUseWhy
You will write every byte before reading anymallocCheapest for small blocks; zeroing would be wasted
You need the memory zeroedcallocCorrect, and for large blocks faster than malloc + memset in the measured case
The element count comes from inputcalloc, or malloc with an explicit overflow checkcalloc rejects a wrapped count × size
A large table that may be sparsely usedcallocUntouched pages were never made resident in the measured case
Growing an existing blockrealloc, into a temporary pointerAvoids leaking the block on failure
C++ codestd::vector, std::make_uniqueConstructors, destructors and ownership handled for you

Key Takeaways

  • malloc leaves memory indeterminate; calloc zeroes it and checks count × size for overflow. Both are released with free.
  • Fresh malloc memory often reads as zero, and recycled memory does not. A reused block still held the previous owner’s ab bytes. Never rely on the zeros.
  • calloc catches the multiplication overflow that malloc cannot. A wrapped size gave malloc a 4-byte block and a silent heap overflow.
  • calloc is 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 failed realloc leaves 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 new and delete, and never mix malloc/free with new/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.

Scroll to Top