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 numbers, where 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>.
| Function | What it does | On failure |
|---|---|---|
void *malloc(size_t n) | Allocates a block of bytes. The contents are uninitialised. (For 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,callocorreallocuntilfreeor until a successfulreallocreplaces it. After a successfulreallocthe 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.
mallocreturnsvoid *. 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
NULLbefore using the result. A failed allocation returnsNULL, and dereferencing it is undefined. - Size idiom.
p = malloc(n * sizeof *p);stays correct if the type ofpchanges.sizeof *pis the size of one pointee.sizeof pis 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 needaligned_alloc. calloczero. 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 meanNULLor0.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 bymalloc,calloc,reallocoraligned_allocthat has not been freed (or replaced byrealloc) yet, orNULL. 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 by | entering a block or calling a function | malloc, calloc, realloc |
| Destroyed by | leaving the block or function (automatic) | free (manual) |
| Size known | at compile time (except variable-length arrays) | chosen at run time |
| Lifetime | until the enclosing block ends | until free |
| Typical mistakes | returning a pointer to a local | leaks, use after free, double free |
| Size limit | small, 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 addressesC source
Stack
ppointer
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.
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 addressesC source
Stack
ppointer
Heap
block Aint
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.
The bug catalogue
| Bug | Typical code | What happens (usual machine) | C standard |
|---|---|---|---|
| Leak | p = malloc(8); p = malloc(8); | first block lost until exit; no immediate crash (repeated leaks can exhaust memory) | not undefined, just waste |
| Use after free | free(p); *p = 1; | may appear to work, may corrupt another allocation | undefined |
| Double free | free(p); free(p); | often a crash or heap corruption | undefined |
| Invalid free | int x; free(&x); or free(p + 1) | crash or corruption | undefined |
| Heap overflow | p = 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 work | undefined |
| Uninitialised read | p = malloc(4); x = *p; | garbage value | indeterminate value; treat it as undefined in exam answers |
| Unchecked failure | p = malloc(n); p[0] = 1; with NULL result | crash | undefined (null dereference) |
| Early-return leak | p = malloc(n); if (bad) return; without free | leak on that path | not 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:
30Reading it. The block has room for 5 ints, used with the same p[i] syntax as an array. The squares are 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 0Reading it. calloc(4, sizeof *p) allocates 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 21Reading 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 40Reading 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: . A question that writes malloc(sizeof p) allocates only 8 bytes.
EX 5Counting how many blocks leak
GATE levelThe 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:
2Reasoning 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 4Reading 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 levelConsider 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): bytes, uninitialised,NULLon 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 forNULL.sizeof pis the pointer, not the block. - A string copy needs
strlen + 1bytes. - Free every block exactly once.
free(NULL)is harmless. - After
free(p): do not usep. Set it toNULL. 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 ifreallocfails.
Traps and GATE patterns
Connections
Builds on
Unlocks
Across subjects
Within this subject
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 answerHide
10 bytes hold only 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 answerHide
(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 answerHide
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.
1Question 4
Why is p = realloc(p, newsize); considered risky?
Show answerHide
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 answerHide
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 forNULL.- Use
n * sizeof *p.sizeof pis 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 changep; the pointer dangles. Set toNULL. 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
My notes (saved in this browser)