Pointers & Memory · intermediate · ~8 min
By the end of this lesson you will be able to: - Use `free` correctly to return heap memory you obtained from `malloc`, `calloc`, or `realloc`. - Pair every allocation with exactly one matching `free` — no leaks, no double frees. - Recognize and avoid the three classic `free` bugs: double-free, use-after-free, and freeing non-heap memory. - Apply the `free(p); p = NULL;` discipline to neutralize dangling pointers. - Decide and document *ownership* — who is responsible for freeing each block — using a `T_new` / `T_free` pairing. - Verify your cleanup with Valgrind and AddressSanitizer so you catch mistakes before users do.
Memory you get from the heap does not clean itself up. In the previous lesson, malloc and the heap, you learned how malloc carves out a block of memory that lives until you decide to release it. free is the other half of that contract: it is the call that ends a block's life and hands the memory back to the allocator so it can be reused.
Why does C make you do this by hand? Languages like Python, Java, and Go have a garbage collector — a background system that figures out when memory is no longer reachable and reclaims it for you. C has no garbage collector. The trade is control for responsibility: you decide exactly when memory is allocated and freed, which is what makes C fast and predictable enough for operating systems, databases, and embedded devices — but it also means a forgotten or mistaken free is your bug to fix.
The central idea of this lesson is ownership. Ownership is not a C keyword; it is a discipline. For every heap block, exactly one part of your program must be the "owner" — the code responsible for eventually calling free on it, exactly once. The language will not track this for you. You track it, in your head and in your function names and comments. Most real-world free bugs are really ownership confusion: two pieces of code each thought they owned a block (double-free), or neither did (leak), or one freed it while the other was still using it (use-after-free).
This lesson teaches the mechanics of free and then the ownership thinking that prevents the bugs. It connects directly to the next lesson, Memory leaks, which is what happens when ownership fails in the "nobody freed it" direction.
Without free, every allocation lasts for the entire life of the program. For a short command-line tool that runs and exits, the operating system reclaims everything on exit, so a missing free is harmless in practice (though still sloppy).
The picture changes completely for long-running programs — web servers, databases, game engines, daemons, and embedded firmware that may run for months without restarting. If such a program leaks even a handful of bytes per request, memory use climbs steadily until the process is killed by the operating system or the device runs out of RAM. This is one of the most common causes of "the server falls over every few days and a restart fixes it."
The bugs around free matter even more. A double-free or use-after-free corrupts the allocator's internal bookkeeping. These are not just crashes — they are among the most exploited vulnerability classes in real software (browsers, kernels, and media libraries have all shipped serious use-after-free CVEs). Even though this is not a security lesson, the habits you learn here are exactly the habits that keep C code out of the security headlines. free is the discipline that keeps long-running C programs both healthy and safe.
free actually doesfree(p) returns a previously allocated block to the heap allocator. The block must have come from an earlier malloc, calloc, or realloc and must not have been freed already. After the call, the allocator may reuse that memory for the next allocation. The pointer variable p still holds the same numeric address, but that address no longer belongs to you.
Before free(p): After free(p):
p ──► [ block you own ] p ──► [ memory now reusable ]
(valid to use) (NOT yours; p is dangling)
The arrow (address in p) does not change.
What changes is your *permission* to touch it.
Knowledge check: After free(p), what is the value stored in p itself? (Answer: the same address as before — free does not modify p. That is exactly why a freed pointer is dangerous.)
Every successful allocation must be matched by exactly one free. Zero frees is a leak. Two frees on the same block is a double-free, which corrupts the allocator. Think of allocation and freeing as a balanced pair, like opening and closing a file.
When to free: as soon as you are truly done with the block and no other part of the program still needs it. Not before. The hard part is knowing when "no one else needs it" — that is the ownership question.
Pitfall: freeing inside a loop the pointer you will reuse next iteration is fine; freeing a pointer that another data structure still references is a use-after-free waiting to happen.
free(p); p = NULL;The moment after you free a block, any pointer still holding its address is a dangling pointer. Reading or writing through it is use-after-free, which is undefined behaviour: the program may crash, silently corrupt whatever was placed in the reused memory, or appear to work — until it doesn't.
The standard defense is to immediately overwrite the pointer:
free(p);
p = NULL;
This "disarms" the pointer. A later accidental dereference of p is now a clean NULL dereference (an immediate, obvious crash) instead of a silent corruption of reused memory. It also makes an accidental second free(p) safe, because free(NULL) is a defined no-op.
free(p); p ──► [ reusable memory ] (dangling — landmine)
p = NULL; p ──► NULL (disarmed — safe)
Knowledge check (predict the output): Given int *p = malloc(sizeof *p); *p = 7; free(p); printf("%d\n", *p); — what happens? (Answer: undefined behaviour. It might print 7, might print garbage, might crash. There is no correct expected output; the bug is dereferencing freed memory.)
Never pass free a pointer that did not come from malloc/calloc/realloc. These are all invalid and corrupt the program:
int x;
free(&x); // x is a stack variable, not heap memory
free("hello"); // string literals live in read-only storage
int a[10];
free(a + 3); // not the start of a heap block
You must also pass the exact pointer malloc returned — not an offset into the block. If you advance a pointer while using a buffer, keep the original to free.
Pitfall: char *p = malloc(10); p++; ... free(p); is a bug — free needs the original address, not p + 1.
The C standard library tracks nothing about who should free a block. You decide and document it. The most reliable pattern is to pair a constructor and destructor per type, inside the module that defines the type:
Thing *thing_new(...) // allocates and initializes — caller now OWNS the result
void thing_free(Thing *t) // frees everything thing_new allocated
The rule becomes simple: whoever called thing_new is responsible for one matching thing_free. When a struct owns nested heap data, its thing_free must free the inner blocks before freeing the struct itself.
Knowledge check (explain in your own words): Why is it a problem if two different functions both believe they own the same block? (Answer: each will eventually call free on it, producing a double-free; or one frees while the other still reads, producing a use-after-free. Exactly one owner avoids both.)
free has a deliberately tiny interface:
void free(void *ptr); // declared in <stdlib.h>; returns nothing
A correct allocate-use-free cycle, annotated:
#include <stdlib.h>
char *buf = malloc(64); // request 64 bytes from the heap
if (buf == NULL) { // malloc can fail — always check
/* handle the error; do NOT use buf */
}
/* ... use buf[0..63] ... */
free(buf); // hand the block back to the heap
buf = NULL; // disarm: any later use is now a clean NULL deref
free(NULL); // explicitly safe — guaranteed to do nothing
Key facts encoded above: free takes any pointer type (it accepts void *), returns nothing, must receive a pointer from the malloc family, and treats NULL as a no-op so you never need to guard a free call with an if.
void free(void *p) releases memory previously returned by malloc, calloc, or realloc.
After the call, the pointer is invalid. Using it is undefined behaviour.
free(NULL) is a safe no-op. You do not need to guard a free call with a NULL check.
The C standard library has no built-in ownership rules. Nothing tracks who is responsible for freeing a block. You must decide and document this yourself.
A common pattern is to pair T_new() and T_free() functions inside a module, so each type owns its own cleanup.
#include <stdio.h> #include <stdlib.h> #include <string.h>
/* A small owned type: a person with a heap-allocated name. */ typedef struct { char name; / owned: allocated in person_new, freed in person_free */ int age; } Person;
/* Constructor: allocates the struct AND a copy of the name. Returns NULL on failure with nothing leaked. Caller owns the result. */ Person *person_new(const char *name, int age) { Person *p = malloc(sizeof *p); if (p == NULL) return NULL;
p->name = malloc(strlen(name) + 1); /* +1 for the terminating '\0' */
if (p->name == NULL) {
free(p); /* clean up the struct before bailing out */
return NULL; /* no leak, no half-built object escapes */
}
strcpy(p->name, name);
p->age = age;
return p;
}
/* Destructor: frees the inner block FIRST, then the struct itself. Safe to call with NULL, mirroring free's own contract. */ void person_free(Person p) { if (p == NULL) return; free(p->name); / free nested heap data before the container */ free(p); }
int main(void) { Person *p = person_new("Ada", 36); if (p == NULL) { fprintf(stderr, "allocation failed\n"); return 1; }
printf("%s is %d\n", p->name, p->age);
person_free(p); /* one matching free for the one person_new */
p = NULL; /* disarm the dangling pointer */
person_free(p); /* safe: person_free(NULL) does nothing */
return 0;
}
We trace the program from main outward.
person_new("Ada", 36) runs first. It calls malloc(sizeof *p) to allocate the Person struct on the heap and checks the result. sizeof *p is the size of one Person, computed from the pointer itself — robust even if the type changes later.strlen(name) + 1 bytes for the name. The + 1 is for the terminating '\0'; forgetting it is a classic off-by-one buffer overflow. If this second allocation fails, the function frees the struct it already allocated and returns NULL — note that without this free(p), a name-allocation failure would leak the struct.strcpy copies "Ada" (4 bytes including the terminator) into the new block, and p->age = 36. The function returns the fully built object. Ownership transfers to main.main, p is checked against NULL, then printf reads p->name and p->age. At this point the heap holds two live blocks owned through p: the struct, and the name buffer it points to.Heap state while printing:
p ──► Person { name ──► "Ada\0" , age = 36 }
(block 1) (block 2)
person_free(p) frees the inner name block first, then the struct. Order matters: if you freed p first, p->name would be a read of freed memory (use-after-free) when you tried to free the name. After this call, both blocks are returned to the heap.p = NULL; disarms the now-dangling pointer.person_free(p) is called again deliberately, with p now NULL. The guard if (p == NULL) return; makes it a no-op — demonstrating why a NULL-safe destructor (and free(NULL) itself) lets calling code be relaxed.| Step | p value |
Block 1 (struct) | Block 2 (name) |
|---|---|---|---|
| after person_new | valid address | live | live |
| after free(p->name) in destructor | valid address | live | freed |
| after free(p) in destructor | dangling | freed | freed |
| after p = NULL | NULL | freed | freed |
Expected output:
Ada is 36
char *s = malloc(16);
free(s);
free(s); // WRONG: same block freed twice — corrupts the allocator
Why it is wrong: the allocator's internal free-list bookkeeping assumes each block is freed once. A second free can corrupt that structure, leading to crashes or exploitable behaviour. Fix it by nulling after freeing, since free(NULL) is safe:
free(s);
s = NULL;
free(s); // now a harmless no-op
Recognize it: crashes or sanitizer reports mentioning "double free" or "invalid free".
int *a = malloc(4 * sizeof *a);
free(a);
a[0] = 1; // WRONG: writing into memory you no longer own
Why it is wrong: the block may already be reused by another allocation, so this silently corrupts unrelated data. Corrected: do all reads/writes before free, and set a = NULL after so a stray use crashes loudly instead of corrupting silently.
Person *p = malloc(sizeof *p);
p->name = malloc(32);
if (p->name == NULL)
return NULL; // WRONG: leaks the struct p
Why it is wrong: the early return abandons p with no way to free it. Corrected: free(p); before the return NULL;, exactly as the lesson's person_new does. Recognize it with Valgrind, which reports the leaked block and the line that allocated it.
char *p = malloc(10);
while (*p) p++; // advancing through the buffer
free(p); // WRONG: p no longer points at the block start
Corrected: keep the original. char *start = malloc(10); char *cur = start; ... free(start);.
char buf[64];
free(buf); // WRONG: buf is on the stack
Corrected: only free what came from malloc/calloc/realloc. Stack arrays, globals, and string literals are never freed.
Compiler errors are rare for free itself, since it accepts any pointer. The most common is implicit declaration of function 'free' — fix by including <stdlib.h>. Compiling with -Wall -Wextra also flags some misuse patterns.
Runtime errors from bad frees often look like free(): double free detected or free(): invalid pointer printed by glibc, followed by an abort. On some systems a bad free just crashes with no message.
Logic errors (leaks) produce no crash at all — the program runs fine but memory grows. These are the hardest to spot by eye.
Concrete debugging steps:
gcc -g -fsanitize=address prog.c and run. It pinpoints use-after-free and double-free with the exact line that allocated, freed, and misused the block.valgrind --leak-check=full ./prog. It reports leaks (which allocation was never freed), invalid frees, and reads/writes of freed memory.Questions to ask when it doesn't work:
malloc in this code path get exactly one free?free? If so, the heap was likely corrupted earlier — the crash site is where the damage surfaced, not where it began. Run under a sanitizer to find the real origin.free sits at the center of three of C's most serious memory-safety problems, so treat it carefully even outside security work.
free(p); p = NULL; and by having a single, clear owner for each block.General robustness rules for this topic: always check malloc before use; free nested data before the container; pass free the exact original pointer; and run every new allocation-heavy code path under AddressSanitizer or Valgrind before trusting it. Every modern static analyzer warns about missing p = NULL and about freed-then-used pointers — heed those warnings.
Where free shows up in real software:
sqlite3_close), libcurl (curl_easy_cleanup), and OpenSSL all expose a *_free/*_close function that is the user's obligation to call — the same T_new/T_free ownership pattern from this lesson, at industrial scale.free only when it drops to zero, so the "owner" is effectively "the last user."Professional best-practice habits:
Beginner rules: pair every malloc with exactly one free; set freed pointers to NULL; never free stack or literal memory; always check malloc before use; free nested data before the container.
Advanced habits: document ownership at every API boundary ("caller frees the returned pointer"); provide a matching *_free for every *_new; make destructors NULL-safe; centralize allocation through a wrapper like xmalloc that aborts on failure when there is no sensible recovery; and gate merges on a clean Valgrind/AddressSanitizer run in continuous integration.
Write long sum_first_n(int n) that allocates an array of n ints on the heap, fills it with 1, 2, ..., n, sums them, frees the array, and returns the sum. Requirements: check the malloc result; free before returning; do not leak. Example: sum_first_n(5) returns 15. Hint: compute the sum into a local variable before you free, never after. Concepts: allocate-use-free cycle, one free per malloc.
Write void str_free(char *s) that is safe to call with NULL and otherwise frees s. Then write a tiny main that allocates a string, frees it via str_free, sets the pointer to NULL, and calls str_free again to prove it is harmless. Requirements: no crash on the second call. Hint: guard with if (s == NULL) return; — or just rely on free(NULL). Concepts: free(NULL), disarming pointers.
Define typedef struct { int *data; int len; } IntList; with IntList *intlist_new(int len) and void intlist_free(IntList *l). The constructor allocates both the struct and its data array; the destructor frees both in the correct order. Requirements: constructor returns NULL with nothing leaked if either allocation fails; destructor is NULL-safe. Verification: run under Valgrind and confirm zero leaks and zero errors. Concepts: ownership, freeing nested data before the container, error-path cleanup.
Write int *concat(const int *a, int na, const int *b, int nb) that returns a freshly allocated array of na + nb ints containing all of a followed by all of b. Requirements: caller owns and must free the result; document that ownership in a comment; handle a NULL return from malloc. Example: concatenating {1,2} and {3} yields {1,2,3}. Hint: use memcpy for each part. Concepts: ownership transfer at an API boundary.
Given typedef struct Node { int val; struct Node *next; } Node;, write Node *remove_nth(Node *head, int n) that removes the node at index n (0-based), frees exactly that node, and returns the (possibly new) head. Requirements: removing the head must work with the same code as removing any other node — use a pointer-to-pointer (Node **). Free only the removed node; leave the rest of the list intact and leak-free. Constraints: if n is out of range, free nothing and return head unchanged. Hint: walk a Node **cur = &head and splice with *cur = (*cur)->next; before freeing. Verification: Valgrind reports no leaks and no invalid frees. Concepts: pointer-to-pointer, freeing one block while preserving others, ownership of a single node.
free(p) ends a heap block's lifetime and returns it to the allocator; the block must have come from malloc/calloc/realloc.free, the pointer is dangling — using it is undefined behaviour. The fix is the reflex free(p); p = NULL;.T_new with a NULL-safe T_free that frees nested data before the container.-fsanitize=address) and Valgrind — with these habits and tools, C programs can be as leak-free and reliable as garbage-collected ones.