Pointers & Memory · intermediate · ~8 min

free and ownership

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.

Overview

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.

Why it matters

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.

Core concepts

What free actually does

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

One free per malloc

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.

Dangling pointers and 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.)

Only free what you malloc'd

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.

Ownership is a discipline, not a language feature

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

Syntax notes

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.

Lesson

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.

Ownership is a discipline

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.

Code examples

#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;

}

Line by line

We trace the program from main outward.

  1. 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.
  2. It then allocates 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.
  3. 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.
  4. Back in 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)
  1. 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.
  2. p = NULL; disarms the now-dangling pointer.
  3. 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

Common mistakes

Mistake 1: Double-free

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".

Mistake 2: Use-after-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.

Mistake 3: Leaking on an error path

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.

Mistake 4: Freeing a moved pointer

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

Mistake 5: Freeing non-heap memory

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.

Debugging tips

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:

  1. AddressSanitizer — compile with 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.
  2. Valgrind — run valgrind --leak-check=full ./prog. It reports leaks (which allocation was never freed), invalid frees, and reads/writes of freed memory.
  3. Set freed pointers to NULL so a use-after-free turns into an immediate, easy-to-find NULL dereference instead of silent corruption.

Questions to ask when it doesn't work:

  • Did every malloc in this code path get exactly one free?
  • Could two pointers be aliasing the same block, leading to a double-free?
  • Is the crash far from the bad 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.

Memory safety

free sits at the center of three of C's most serious memory-safety problems, so treat it carefully even outside security work.

  • Double-free corrupts the allocator's metadata. Beyond crashes, it has historically been turned into a write-anywhere primitive by attackers. Prevent it with free(p); p = NULL; and by having a single, clear owner for each block.
  • Use-after-free lets stale data — or, in an adversarial setting, attacker-chosen data placed in the reused block — flow into your program through a dangling pointer. Prevent it by nulling freed pointers and by never freeing memory another live pointer still depends on.
  • Leaks are the inverse: memory whose owner forgot to free it. Not undefined behaviour, but fatal to long-running processes. The next lesson, Memory leaks, covers these in depth.

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.

Real-world uses

Where free shows up in real software:

  • End-of-request cleanup in servers. A web server allocates buffers, parsed headers, and response bodies per request; each must be freed when the request completes, or the server slowly bloats and falls over.
  • Destructors in C libraries. APIs like SQLite (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.
  • Reference counting. When a block is shared, libraries often track an integer count and call 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.

Practice tasks

Beginner 1 — Allocate, use, free

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.

Beginner 2 — NULL-safe destructor

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.

Intermediate 1 — Constructor/destructor pair

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.

Intermediate 2 — Concatenate two arrays

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.

Challenge — Remove and free the n-th list node

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.

Summary

  • free(p) ends a heap block's lifetime and returns it to the allocator; the block must have come from malloc/calloc/realloc.
  • After free, the pointer is dangling — using it is undefined behaviour. The fix is the reflex free(p); p = NULL;.
  • Match every allocation with exactly one free: zero frees leak, two frees corrupt the allocator (double-free).
  • Only free what you malloc'd, and pass the exact original pointer — never a stack variable, literal, or offset.
  • Ownership is a discipline you enforce, not a language feature: give each block one owner, and pair T_new with a NULL-safe T_free that frees nested data before the container.
  • Free nested blocks before their container, and clean up on error paths to avoid leaks.
  • Verify with AddressSanitizer (-fsanitize=address) and Valgrind — with these habits and tools, C programs can be as leak-free and reliable as garbage-collected ones.

Practice with these exercises