G27
Self-checkedTier 1 17 min readpds.dynamic-memory

Dynamic memory: malloc, free and leaks

How a C program asks for memory while it runs, what malloc, calloc, realloc and free promise, and the catalogue of bugs that follow when the rules are broken (leak, use after free, double free, overflow). Includes how to count leaked blocks in an exam question.

Why it exists

Arrays and local variables have a size fixed in the source and a lifetime tied to a block. Real programs often need neither: read nn numbers, where nn is typed by the user, or build a list that grows, or return a result that must outlive the function that made it. For this the language offers dynamic memory: the program asks the system for a block of a chosen size while it runs, uses it, and gives it back when it is done. The block lives in the heap (see the memory layout page) and its lifetime is chosen by the programmer, not by the block structure.

The price is responsibility. Nothing gives the block back automatically. A program that forgets, loses memory (a leak). A program that gives it back and then keeps using it breaks in ways that are hard to find.

Prerequisites

Precise definitions

All four functions are declared in <stdlib.h>.

FunctionWhat it doesOn failure
void *malloc(size_t n)Allocates a block of nn bytes. The contents are uninitialised. (For n=0n = 0 the result is implementation-defined: NULL or a unique pointer that must not be dereferenced. Avoid it.)returns NULL
void *calloc(size_t count, size_t size)Allocates room for count objects of size bytes and sets all bits to zero. (If count * size would overflow, it fails instead of wrapping.)returns NULL
void *realloc(void *p, size_t n)Changes the size of the block p points to. The old contents are kept (up to the smaller of the two sizes). The block may move, so use the returned pointer. If p is NULL it behaves like malloc. Do not pass a size of 0 (implementation-defined, and some libraries then free the block).returns NULL and the old block stays valid
void free(void *p)Gives the block back. free(NULL) does nothing.none

Rules and definitions:

  • Allocated storage duration. The block exists from the successful call to malloc, calloc or realloc until free or until a successful realloc replaces it. After a successful realloc the old pointer is indeterminate, even if the new address happens to be the same, and any other pointers into the old block dangle.
  • The block has no name. It is reachable only through pointers that hold its address. The pointer variable itself is an ordinary local or global, with its own lifetime. Losing the last pointer means losing the block.
  • malloc returns void *. In C it converts to any object pointer without a cast. Writing a cast is allowed but unnecessary (C++ is different: there the cast is required).
  • Always check for NULL before using the result. A failed allocation returns NULL, and dereferencing it is undefined.
  • Size idiom. p = malloc(n * sizeof *p); stays correct if the type of p changes. sizeof *p is the size of one pointee. sizeof p is the size of the pointer itself (8 bytes on a 64-bit machine), a classic mistake.
  • Alignment. The returned address is suitably aligned for any type with a fundamental alignment requirement (up to max_align_t, 16 bytes on x86-64 gcc). Over-aligned types need aligned_alloc.
  • calloc zero. The bits are all zero. For integers that is the value 0. For pointers and floating-point numbers, the standard does not promise that all-zero bits mean NULL or 0.0, although they do on common machines.
  • After free. The block no longer belongs to the program. The value of the pointer becomes indeterminate (C11 6.2.4): using it, even only to compare it, is not allowed. Real machines leave the old address in the variable, which is what makes this bug dangerous (the animation shows it).
  • Valid arguments to free. Only a pointer returned by malloc, calloc, realloc or aligned_alloc that has not been freed (or replaced by realloc) yet, or NULL. Anything else (a stack address, a pointer into the middle of a block, a block already freed) is undefined behaviour.
  • Memory leak. A block that is still allocated but that the program can no longer reach: it cannot be found by starting from the live variables (globals and locals in scope) and following pointers. Overwriting the last pointer to a block, or letting the last pointer go out of scope, makes it unreachable. A block that is only referenced from inside other unreachable blocks (for example the second node of a list whose first node was lost) is also lost. A single leaked block does not crash anything and is reclaimed when the program ends, but leaks that repeat in a loop or a long-running program eventually use up memory. The C standard has no concept of a leak; it is a property that tools such as AddressSanitizer's leak checker report. Whether a block is unreachable can be decided by drawing the pointers.

Stack versus heap

Stack (automatic)Heap (allocated)
Created byentering a block or calling a functionmalloc, calloc, realloc
Destroyed byleaving the block or function (automatic)free (manual)
Size knownat compile time (except variable-length arrays)chosen at run time
Lifetimeuntil the enclosing block endsuntil free
Typical mistakesreturning a pointer to a localleaks, use after free, double free
Size limitsmall, platform-dependent (a few megabytes is typical)much larger, limited by the system

How it works

One block, from birth to death

Step through the run. Notice (1) the heap block has no name of its own; the stack pointer p is its only handle. (2) q = p makes two pointers to one block. (3) free crosses out the block, but the pointers keep their old addresses and now point at freed memory. Writing p = NULL fixes p only; the copy q stays dangling.

malloc, use, free, dangling

Real run, real addresses

C source

1#include <stdio.h>
2#include <stdlib.h>
3
4int main(void) {
5 int *p = NULL;
6 p = malloc(3 * sizeof(int));
7 p[0] = 7;
8 p[1] = 8;
9 p[2] = 9;
10 int *q = p;
11 free(p);
12 p = NULL;
13 return 0;
14}

Stack

ppointer

NULL…d5f8

Heap

nothing here yet

Amber = changed in this step. Hover a cell for its full address. Arrows show where a pointer points; the small tag under a cell names the pointers aimed at it.

Step 1

p is a normal stack variable. It starts as NULL: no block yet.

1 / 6
A heap block is created by malloc and lives until free. A pointer to it does not know when it dies. Produced by: real execution: every address and value is read from memory by the running program at each step. gcc (Debian 14.2.0-19) 14.2.0. gcc -O0 -o prog prog.c && setarch x86_64 -R ./prog (ASLR off so the numbers are stable)

How a leak happens

In this run the only pointer to block A is overwritten by a new malloc. The animation computes, from the program's real pointers, which live blocks nobody can reach, and flags block A. The block is still allocated, still holds 1, 2, and can never be freed.

A memory leak

Real run, real addresses

C source

1#include <stdio.h>
2#include <stdlib.h>
3
4int main(void) {
5 int *p = malloc(2 * sizeof(int));
6 p[0] = 1;
7 p[1] = 2;
8 p = malloc(4 * sizeof(int));
9 free(p);
10 return 0;
11}

Stack

ppointer

block A[0]…d5f8

Heap

block Aint

?…a2a0
[0]
↑p
?…a2a4
[1]

Amber = changed in this step. Hover a cell for its full address. Arrows show where a pointer points; the small tag under a cell names the pointers aimed at it.

Step 1

p points at a new 2-int block, block A.

1 / 4
A leaked block is one that is still allocated but that no pointer can reach, so it can never be freed. Produced by: real execution: every address and value is read from memory by the running program at each step. gcc (Debian 14.2.0-19) 14.2.0. gcc -O0 -o prog prog.c && setarch x86_64 -R ./prog (ASLR off so the numbers are stable)

The bug catalogue

BugTypical codeWhat happens (usual machine)C standard
Leakp = malloc(8); p = malloc(8);first block lost until exit; no immediate crash (repeated leaks can exhaust memory)not undefined, just waste
Use after freefree(p); *p = 1;may appear to work, may corrupt another allocationundefined
Double freefree(p); free(p);often a crash or heap corruptionundefined
Invalid freeint x; free(&x); or free(p + 1)crash or corruptionundefined
Heap overflowp = malloc(4 * sizeof *p); p[4] = 0;may overwrite a neighbouring block or the allocator's bookkeeping, or land in unused slack and seem to workundefined
Uninitialised readp = malloc(4); x = *p;garbage valueindeterminate value; treat it as undefined in exam answers
Unchecked failurep = malloc(n); p[0] = 1; with NULL resultcrashundefined (null dereference)
Early-return leakp = malloc(n); if (bad) return; without freeleak on that pathnot undefined

Worked examples

EX 1An array whose size is chosen at run time

Easy
#include <stdio.h>
#include <stdlib.h>
 
int main(void) {
    int n = 5;
    int *p = malloc(n * sizeof *p);
    if (p == NULL) return 1;
    int sum = 0;
    for (int i = 0; i < n; i++) {
        p[i] = i * i;
        sum += p[i];
    }
    printf("%d\n", sum);
    free(p);
    return 0;
}

Output:

30

Reading it. The block has room for 5 ints, used with the same p[i] syntax as an array. The squares are 0,1,4,9,160, 1, 4, 9, 16 and their sum is 30. The block is freed once, at the end, and p is not used afterwards.

EX 2calloc zeroes, malloc does not

Easy
#include <stdio.h>
#include <stdlib.h>
 
int main(void) {
    int *p = calloc(4, sizeof *p);
    if (p == NULL) return 1;
    printf("%d %d %d %d\n", p[0], p[1], p[2], p[3]);
    free(p);
    return 0;
}

Output:

0 0 0 0

Reading it. calloc(4, sizeof *p) allocates 4⋅4=164 \cdot 4 = 16 bytes and sets them to zero. With malloc(16) the same four values would be indeterminate, so printing them would be a bug.

EX 3realloc keeps the old contents

Medium
#include <stdio.h>
#include <stdlib.h>
 
int main(void) {
    int *p = malloc(3 * sizeof *p);
    if (p == NULL) return 1;
    for (int i = 0; i < 3; i++) p[i] = i + 1;
    int *t = realloc(p, 6 * sizeof *p);
    if (t == NULL) {
        free(p);
        return 1;
    }
    p = t;
    for (int i = 3; i < 6; i++) p[i] = i + 1;
    int s = 0;
    for (int i = 0; i < 6; i++) s += p[i];
    printf("%d %d\n", p[0], s);
    free(p);
    return 0;
}

Output:

1 21

Reading it. After growing the block from 3 to 6 ints, the first three values (1, 2, 3) are still there, even if the block moved. The new cells are filled with 4, 5, 6, so the sum is 21. The result goes into a temporary t first. Writing p = realloc(p, ...) would lose the only pointer to the old block if realloc failed and returned NULL: that is a leak.

EX 4sizeof the pointer is not the size of the block

Medium
#include <stdio.h>
#include <stdlib.h>
 
int main(void) {
    int *p = malloc(10 * sizeof *p);
    if (p == NULL) return 1;
    printf("%zu %zu %zu\n", sizeof p, sizeof *p, 10 * sizeof *p);
    free(p);
    return 0;
}

Output (64-bit):

8 4 40

Reading it. sizeof p is the size of the pointer variable, 8. It says nothing about the block. sizeof *p is one int, 4. The block size is something the program has to remember itself: 10⋅4=4010 \cdot 4 = 40. A question that writes malloc(sizeof p) allocates only 8 bytes.

EX 5Counting how many blocks leak

GATE level

The program below wraps malloc and free in two small helpers that keep a count of blocks currently allocated. This is just a way to put a number on what the diagram shows.

#include <stdio.h>
#include <stdlib.h>
 
static int live;
 
static int *grab(void) {
    live++;
    return malloc(sizeof(int));
}
 
static void drop(int *p) {
    live--;
    free(p);
}
 
int main(void) {
    int *p = NULL;
    for (int i = 0; i < 3; i++) {
        p = grab();
        if (p == NULL) return 1;
        *p = i;
    }
    drop(p);
    printf("%d\n", live);
    return 0;
}

Output:

2

Reasoning with pictures. The loop runs three times and each time p is overwritten with a new block. After iteration 1, block 1 is reachable through p. After iteration 2, p points to block 2 and block 1 has no pointer: it has leaked. After iteration 3, block 2 has leaked, and p points to block 3. drop(p) frees block 3. So 2 blocks are allocated and unreachable at the end.

The rule for such questions. Draw one pointer variable and one box per malloc. Every time a pointer is overwritten or goes out of scope, check whether another pointer still holds that block's address. Blocks with no remaining pointer that were not freed are the leaks. If blocks contain pointers to each other (a list), a block is also lost when the only pointers to it sit inside blocks that are themselves lost. (Tools distinguish a block that is lost from one that is still reachable at exit, for example through a global pointer. The program forgot to free it, but it was never unreachable. Exam questions normally count any block that is never freed and has lost all its pointers.)

EX 6Returning memory from a function, and the + 1

GATE level
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
static char *dup(const char *s) {
    char *d = malloc(strlen(s) + 1);
    if (d == NULL) return NULL;
    strcpy(d, s);
    return d;
}
 
int main(void) {
    char *c = dup("GATE");
    if (c == NULL) return 1;
    c[0] = 'g';
    printf("%s %zu\n", c, strlen(c));
    free(c);
    return 0;
}

Output:

gATE 4

Reading it. strlen(s) is 4 but the copy needs 5 bytes, because the null character must be copied too. malloc(strlen(s)) would allocate one byte too few and strcpy would write past the end: a heap overflow. The block belongs to the caller, who modifies it (it is a private writable copy, unlike the literal) and must free it. Returning a pointer to a local array from such a function would be a dangling pointer. Returning a pointer to a heap block is correct, provided somebody frees it.

EX 7Which line is the leak?

GATE level

Consider this function (not run, because the point is the reasoning).

int *make(int n) {
    int *p = malloc(n * sizeof *p);
    for (int i = 0; i < n; i++) p[i] = 0;
    return p;
}
 
void use(void) {
    int *a = make(10);
    a = make(5);
    free(a);
}

Trace. make(10) returns a block of 40 bytes, held by a. a = make(5) stores the address of a new 20-byte block in a; the only pointer to the 40-byte block is gone. free(a) frees the 20-byte block. So the first block is leaked: 40 bytes, one block. The fix is to call free(a) before a = make(5).

Cheat box

Cheat box

  • malloc(n): nn bytes, uninitialised, NULL on failure. calloc(c, s): zeroed. realloc(p, n): resize, may move, use the result. free(p): give back.
  • Use p = malloc(n * sizeof *p). Check for NULL. sizeof p is the pointer, not the block.
  • A string copy needs strlen + 1 bytes.
  • Free every block exactly once. free(NULL) is harmless.
  • After free(p): do not use p. Set it to NULL. Other copies stay dangling.
  • Leak = allocated, unreachable, never freed (blocks reachable only from lost blocks are lost too). Count by drawing pointers and boxes.
  • Bugs that are undefined: use after free, double free, invalid free, heap overflow, NULL dereference. A leak is not undefined.
  • p = realloc(p, n) can leak the old block if realloc fails.

Traps and GATE patterns

Connections

The heap region and the other regions of a program are laid out on the memory layout page. The structure that makes dynamic memory essential is the singly linked list, where every node is a separate block. At the operating-system level, the heap manager faces the same questions as memory management (fragmentation, free lists), and the heap is one part of the process address space.

PYQs

No PYQs attached to this concept yet

Previous year questions are being added section by section from your PYQ book, each with my own re-checked answer. Nothing is shown from memory.

Self-check

Question 1

int *p = malloc(10); on a machine with 4-byte ints. How many ints does the block really have room for, and what should the argument be to hold 10 ints?

Show answer

10 bytes hold only ⌊10/4⌋=2\lfloor 10 / 4 \rfloor = 2 ints. To hold 10 ints, ask for 10 * sizeof *p, which is 40 bytes.

Question 2

Which of these are undefined behaviour? (a) free(NULL); (b) p = malloc(4); free(p); free(p); (c) p = malloc(4); p = malloc(4); (d) int x; free(&x);

Show answer

(a) is well defined and does nothing. (b) is a double free: undefined. (c) leaks the first block but is not undefined. (d) frees a non-heap address: undefined.

Question 3

How many blocks are still allocated and unreachable when this program ends?

#include <stdio.h>
#include <stdlib.h>
 
static int live;
 
static void *get(size_t n) {
    live++;
    return malloc(n);
}
 
int main(void) {
    int *a = get(sizeof *a);
    int *b = get(sizeof *b);
    if (a == NULL || b == NULL) return 1;
    a = b;
    free(a);
    live--;
    printf("%d\n", live);
    return 0;
}
Show answer

a = b overwrites the only pointer to the first block, so that block is lost. free(a) then frees the second block (reached through a, which now holds b's address), and b becomes dangling. One block was allocated and never freed and is unreachable, so the count is 1.

1

Question 4

Why is p = realloc(p, newsize); considered risky?

Show answer

If realloc cannot grow the block it returns NULL but leaves the original block allocated. The assignment overwrites p with NULL, so the only pointer to the original block is lost: a leak (and p is now NULL).

Question 5

After free(p), a program prints *p and gets the old value. Does that show the code is correct?

Show answer

No. The behaviour is undefined. The memory has not necessarily been reused yet, so the old bytes may still be there, but a later allocation, a different compiler or a different run can give any result.

Revision card

One-screen revision

  • malloc (uninitialised), calloc (zeroed), realloc (resize, may move, assign to a temporary), free. Check for NULL.
  • Use n * sizeof *p. sizeof p is only the pointer. A string copy needs length + 1.
  • The block has no name. Lose the last pointer and it leaks.
  • free(p) does not change p; the pointer dangles. Set to NULL. Copies still dangle.
  • Undefined: use after free, double free, free of a non-heap address or of an interior pointer, heap overflow, NULL dereference.
  • Not undefined: a leak. Count leaks with boxes and arrows.

References

  • ·Kernighan and Ritchie, The C Programming Language, 2nd ed., Sections 5.4 Address Arithmetic, 6.5 Self-referential Structures, 8.7 Example: A Storage Allocator, Appendix B5 Utility Functions (malloc, calloc, realloc, free)
  • ·ISO/IEC 9899:2011 (C11), Sections 6.2.4 Storage durations of objects, 7.22.3 Memory management functions (aligned_alloc, calloc, free, malloc, realloc)

Verification

The two animations replay real runs of instrumented C programs: every address, value and pointer target is read from memory, and the leaked flag is computed by scanning the program's real pointers. The test suite checks that block A is flagged as leaked exactly when its last pointer is overwritten. All C programs on this page are compiled with -Wall -Wextra -Werror and run by npm run verify:c; the leak-counting example was additionally run once under AddressSanitizer's leak detector, which reported the same number of leaked blocks. The textbook section numbers were written from memory and have NOT been checked against the books. The prose, the bug catalogue and the GATE-pattern claims have NOT yet had a human textbook check (the independent second pass is described at the end of these notes). SECOND PASS (2026-11-02, done by the assistant, not a human check): every Self-check question was solved before the printed answer was read and all agreed; the definitions, worked examples, cheat box and traps were read claim by claim and the numbers re-derived by hand (long C programs were not re-read line by line: they are compiled and compared by npm run verify:c, and their printed outputs were checked against the prose). No defect found. Status raised to self-checked; verified needs a textbook or human check.

last verified: never