student@ubuntu:~$
pointers-memory Lesson 8 13 min read

Dynamic Allocation: malloc, calloc, realloc, free

The canonical idiom, the three rules, and the tmp-idiom for resizing

Based on content from Dr. Stu Steiner, Eastern Washington University.

Reading: Hanly & Koffman: §13.1–13.2 (pp. 706–716); K&R §7.8.5 (pp. 167–169)

In a nutshell

The standard library gives you four functions for managing heap memory: malloc (give me n bytes), calloc (give me n zeroed objects), realloc (resize the block I already have), and free (take this block back). Every one of them has one boundary condition that, if you get it wrong, becomes a leak, a crash, or an exploit. This lesson is the small set of patterns that get the boundaries right. The canonical allocation idiom is T *p = malloc(n * sizeof *p); followed by if (p == NULL). The canonical resize idiom uses a temporary: int *tmp = realloc(p, newSize); then commit p = tmp; only if tmp is non-null. The canonical free idiom is free(p); p = NULL;. Memorize those three patterns and you avoid most of the shipping-C bug reports that have ever been filed.

Practice this topic: Dynamic Memory drill, or browse the practice gallery.

After this lesson, you will be able to:

  • Call malloc, calloc, and realloc with correct argument shape and check their return
  • Explain why sizeof *p is preferred over sizeof(T) in the allocation idiom
  • Use the tmp-idiom for realloc and say what breaks when you do not
  • Apply the three rules of free: exactly one per block, only on what malloc returned, never twice
  • Predict what Valgrind will say about a given allocation pattern

Quick reference

Call What you ask for Returns Zeroed?
malloc(nbytes) nbytes bytes void * or NULL no
calloc(n, size) n * size bytes, overflow-checked void * or NULL yes
realloc(p, nbytes) resize p’s block to nbytes void * or NULL old data preserved
free(p) return the block to the allocator (nothing) (n/a)

Coming from CSCD 210

Java has one allocation path (new) and a garbage collector that reclaims objects automatically when they are no longer reachable. new int[huge] throws OutOfMemoryError on failure, which your caller can catch. C has four allocation calls and no garbage collector: you own every byte you ask for, and the return value is the error channel. There is no exception to catch. If malloc fails and you proceed, your next store is almost certainly a segfault (or worse, a silent corruption). The discipline that replaces the GC is the pair of idioms in this lesson.


Getting bytes: malloc and calloc

malloc: the rental office

#include <stdlib.h>

void *malloc(size_t n);

Three things to notice about the signature before writing any code.

  1. It takes size_t, which is an unsigned integer type that holds sizes and indices. The argument is a byte count, not an element count.
  2. It returns void *, the generic pointer type. In C (not C++), void * implicitly converts to any typed object pointer, so you do not cast the result.
  3. It can fail. On failure it returns NULL, and the failure is not theoretical: a stressed grader machine, a 5 a.m. autograder run, or a bug in your loop bounds can all trigger it.

The canonical idiom

T *p = malloc(n * sizeof *p);
if (p == NULL) {
    /* handle failure */
}

Commit that to muscle memory. Three patterns live inside those two lines.

sizeof *p, not sizeof(T). The form sizeof *p reads as “the size of whatever p points to.” If you later change p’s type from int * to long *, the allocation scales itself. If you had hardcoded sizeof(int), you would silently allocate half the bytes and the next loop would scribble out of bounds. sizeof does not evaluate its operand (§6.5.3.4), so sizeof *p is safe even while p is uninitialized; the compiler answers the question at compile time using the declared type.

Check every return. if (p == NULL) ... on the very next line, always. CERT C rule ERR33-C requires it. The handler depends on context: a CLI tool exits with a message; a library function returns an error code; a server logs and drops the request. What is never acceptable is to proceed as if the allocation succeeded.

No cast in C. int *p = malloc(...) is clean. int *p = (int *)malloc(...) is legal but considered noise in C. Worse, the cast masks a missing #include <stdlib.h>: without the header, the compiler would complain about converting int (its default return type for unknown functions) to int *, and catch your missing include at compile time. The cast silences that warning and lets the bug reach runtime on a 64-bit machine where the pointer gets truncated to 32 bits.

What malloc returns

ISO C §7.22.3.4: the returned bytes are uninitialized. Whatever the previous tenant left there is what you get. So:

int *p = malloc(10 * sizeof *p);
if (p == NULL) return 1;
printf("%d\n", p[0]);   /* garbage: might be 0, might not */

If you need zeros, use calloc or follow malloc with an explicit memset. The returned pointer is suitably aligned for any standard type, so you can assign it to int *, double *, or any struct foo * without alignment concerns.

Check your understanding (spot the bug)

#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    int n = atoi(argv[1]);
    int *buf = (int *) malloc(n * sizeof(int));
    for (int i = 0; i < n; i++) buf[i] = i;
    printf("%d\n", buf[n - 1]);
    return 0;
}

Three separate problems hide in those lines.

Reveal answer
  1. No NULL check. malloc can fail; if it does, buf[0] = 0 segfaults. Add if (buf == NULL) return 1; after the allocation.
  2. The (int *) cast. In C, the cast is noise and can hide a missing <stdlib.h>. Remove it. (The code has #include <stdlib.h>, so no live bug here, but the cast still provides no benefit.)
  3. n * sizeof(int) for an untrusted n. argv[1] is attacker-controlled. For a large value, n * sizeof(int) can overflow size_t and wrap to a tiny allocation; the loop then writes out of bounds for a billion iterations. The fix is calloc(n, sizeof *buf), which the standard requires to detect overflow.

A second missing check: no if (buf == NULL) free(buf); on exit. In this program return 0; lets the OS reclaim, but in a long-running process that would be a leak.


calloc: zeroed allocation with an overflow check

void *calloc(size_t nmemb, size_t size);

Two improvements over malloc in one call.

Overflow-checked multiplication. calloc(n, size) computes n * size inside the library and rejects overflow. An attacker-controlled n that would have wrapped malloc(n * sizeof *p) into a tiny allocation produces a clean NULL return from calloc. (This is the integer-overflow-at-allocation bug class; the memory-safety deep dive walks through the named exploits.)

Zero-filled bytes. Every byte in the returned block is all-bits-zero. On any platform you will meet in CSCD 240 this means integer 0, IEEE 0.0, and NULL for pointer fields.

int *counts = calloc(n, sizeof *counts);
if (counts == NULL) return 1;
/* counts[0] .. counts[n-1] are all 0 */

When to use which

Use calloc when n comes from user input, a file, or the network (the overflow check matters) or when you will read before writing most of the elements (the zero-fill is what you wanted anyway). Use malloc when you are about to overwrite every byte in the very next loop and the zero-fill would be wasted work. For Lab 6, calloc is a safe default. If a grep finds malloc(n * sizeof ...) with a user-provided n, rewrite it to calloc(n, sizeof ...).

Check your understanding (predict Valgrind)

int *buf = malloc(5 * sizeof *buf);
if (buf == NULL) return 1;
buf[0] = 1;
buf[1] = 2;
printf("%d\n", buf[3]);     /* note: 3, not 0 or 1 */
free(buf);

What does Valgrind say?

Reveal answer

“Conditional jump or move depends on uninitialised value(s)” or, more likely, an “Uninitialized value was created by a heap allocation” message pointing at printf.

buf[3] was never assigned. malloc does not zero memory, so reading buf[3] reads whatever bytes were in the arena from a previous tenant. Valgrind’s shadow memory knows those bytes are marked UNDEFINED and flags the read.

The fix is either calloc (zeros the bytes so the read is defined) or an explicit initialization (buf[3] = 0; before the read). The deeper lesson: do not read a heap byte that your program has not written.


realloc: resizing without leaking

void *realloc(void *ptr, size_t size);

Three possible outcomes, and they look identical to your code:

  • A (extended in place). The bytes after the current block are free; the allocator extends the block without moving it. realloc returns the same address.
  • B (moved). The allocator picks a different location, copies your old data there, frees the old block, and returns the new address.
  • C (failed). The allocator cannot satisfy the request. realloc returns NULL and leaves the old block untouched.

You cannot tell A from B at the language level, and you must not try. The caller’s rule is: treat any pointer that aliased the old block as invalidated, because a move may have happened.

The realloc trap: p = realloc(p, n) is a bug

Read that line for each outcome:

  • Outcome A: realloc returns the same address. p = p; is a no-op. Fine.
  • Outcome B: realloc returns a new address, frees the old. p is updated to the new address. Fine.
  • Outcome C: realloc returns NULL. The old block is still allocated. p is now NULL, and the only pointer to that still-live block is gone. You just leaked it.

CERT C rule MEM04-C specifically calls this out. Every resizing routine in reviewed C code uses the tmp-idiom instead.

The tmp-idiom

int *tmp = realloc(buf, new_size);
if (tmp == NULL) {
    /* buf is still valid with the old contents.           */
    /* Decide: return error, keep the old buffer, or abort. */
    return -1;
}
buf = tmp;                  /* commit the new pointer */

Walking the three outcomes again:

  • A: tmp is the old address, buf = tmp; is a no-op. Correct.
  • B: tmp is a new address, the old block is already freed by the library; we update buf. Correct.
  • C: tmp == NULL, we bail out; buf is untouched and still valid. Correct.

One convenience: realloc(NULL, n) behaves like malloc(n)

From ISO §7.22.3.5: “If ptr is a null pointer, the realloc function behaves like the malloc function for the specified size.” That lets you write a growable-array routine without a special first-call branch:

int *arr = NULL;
size_t len = 0;

/* later, in a loop that grows arr by one element */
int *tmp = realloc(arr, (len + 1) * sizeof *arr);
if (tmp == NULL) { /* handle */ }
arr = tmp;
arr[len++] = newValue;

On the first iteration, arr is NULL, and the call becomes malloc. Every later iteration resizes. One code path, no special case.

One non-convenience: realloc(p, 0) is a footgun. C17 makes it implementation-defined; C23 makes it undefined. Do not use it to free a block. Use free(p); p = NULL; instead.


free and the three rules

void free(void *ptr);

free takes a pointer and returns the underlying block to the allocator. ISO C §7.22.3.3: ptr must be either NULL (a well-defined no-op) or a value previously returned by malloc, calloc, or realloc and not yet freed. Anything else is undefined behavior.

Two things free does not do:

  1. It does not shrink your process’s address space. The allocator holds onto the memory for future requests.
  2. It does not modify the pointer variable you passed. After free(p), the variable p still holds the same address; the memory at that address no longer belongs to you. That is a dangling pointer.

Rule 1: free every block exactly once before the program ends

Forgetting is a memory leak (CWE-401, CERT MEM31-C). Your program’s resident memory grows; the OS reclaims everything at exit. That makes leaks invisible in short-running programs and catastrophic in long-running ones. Valgrind detects them deterministically; the next lesson walks through the report format.

Rule 2: only free what malloc/calloc/realloc returned

Not stack addresses. Not string literals. Not offsets into the middle of a heap block.

char *s = "hello";
free(s);                 /* WRONG: s points into .text */

int arr[10];
free(arr);               /* WRONG: arr is a stack address */

char *p = malloc(100);
free(p + 5);             /* WRONG: must free exactly what malloc returned */

All three corrupt the allocator’s internal free list, which is the foundation for the heap-corruption exploits catalogued in the memory-safety deep dive.

The companion rule is never use a pointer after freeing it. Use-after-free (CWE-416) sits in MITRE’s Top-25 every year; Chromium attributes roughly a third of its security bugs to it. The defensive habit:

free(p);
p = NULL;

One line. A later accidental *p becomes a deterministic crash instead of silent reuse of freed memory. A later accidental free(p) becomes free(NULL), which is a guaranteed no-op. Two bug classes defused.

Rule 3: never free the same block twice

Double-free (CWE-415) corrupts allocator bookkeeping by inserting the same block into the free list twice. Subsequent malloc calls hand out overlapping addresses to two callers who then stomp on each other’s data. In shipping software this has repeatedly been chained into remote code execution.

The p = NULL; habit from Rule 2 also defends against Rule 3: the second free(p) becomes free(NULL), which does nothing.

Watch for aliased pointers

Rule 3’s trap is that the compiler does not track heap ownership. Two int * variables holding the same address look identical to the type system, and freeing both is a double-free even though neither call site looks suspicious.

int *a = malloc(100);
int *b = a;              /* two names, one block */
free(a);
free(b);                 /* double-free */

Decide who owns each heap block and make sure exactly one place frees it. This is a logical contract, not a syntactic one.

For the named exploits behind each bug class (Morris Worm, Apple goto fail, Baron Samedit, Heartbleed, WhatsApp GIF), see the memory-safety deep dive. For the allocator-level picture of what malloc and free actually do (arenas, the free list, why bad-free and double-free corrupt the list), see the pointer mechanics deep dive. The next lesson introduces Valgrind, the tool that tells you whether your code obeys the three rules.

Check your understanding (Rule violations)

Match each snippet to the rule it violates.

/* A */
char buf[100];
free(buf);

/* B */
int *p = malloc(40);
free(p);
*p = 42;

/* C */
int *p = malloc(40);
free(p);
free(p);

/* D */
int *p = malloc(40);
/* ... program ends without calling free(p) ... */
Reveal answer
  • A violates Rule 2. buf is a stack address, not a heap allocation. Undefined behavior; almost always crashes.
  • B is a use-after-free (Rule 2’s companion rule). The bytes at *p may already be reused by a later malloc; writing there corrupts the new owner’s data.
  • C violates Rule 3. Double-free; allocator bookkeeping is now corrupt.
  • D violates Rule 1. Memory leak. The OS reclaims at exit, so a tiny program appears fine; a long-running program leaks until it runs out of memory.

The free(p); p = NULL; habit catches B and C at the cost of one line.


What comes next

Which statements describe correct use of the allocator API?
AT *p = malloc(n * sizeof *p); is preferred over T *p = malloc(n * sizeof(T)); because the sizeof *p form tracks p's type automatically.
Bp = realloc(p, new_size); is safe as long as new_size is larger than the current size.
Ccalloc(n, sizeof *p) detects multiplication overflow that malloc(n * sizeof *p) would silently wrap.
Dfree(NULL) is a well-defined no-op.
EAfter free(p);, the value of p is set to NULL by the standard library.
Correct: A, C, D.
  • B is wrong: the p = realloc(p, ...) pattern leaks the original block on the failure path because realloc returns NULL without freeing the old block. Use the tmp-idiom.
  • E is wrong: free cannot modify the caller's pointer variable (C is pass-by-value; free received a copy of the pointer). Nulling p is your responsibility.

You now have a working allocator API and the three idioms that keep it from biting. Next, Debugging Memory with Valgrind introduces the tool that actually tells you whether your code obeys the rules: the four leak categories, the invalid-read and uninitialized-value reports, and the exact command line that produces the Lab 6 deliverable. Drill this page with the Dynamic Memory skill card, or browse the practice gallery.