The Heap and Data Lifetime
Why a program needs memory it can ask for
Section titled “Why a program needs memory it can ask for”The previous lesson gave every call its own block of stack space, handed out on the way in and taken back on the way out. That gives local values a convenient lifetime, but it doesn’t suit every kind of data.
The first is timing. An automatic local object’s lifetime ends when execution leaves its block or function. A function can return a copy of its value, but it can’t return a pointer to that local object and expect the caller to keep using it. The second is capacity. Automatic storage is limited, even on systems that permit runtime-sized stack allocations. A large or growing data structure needs a different allocation strategy.
Both limits bite constantly. Read a file with no idea of its length until you open it, build up a list and keep growing it as the program runs, or build data in a helper and let the caller keep using the same allocation after the helper returns.
So we need storage we can request at runtime and keep after a function returns. The request can still fail, and available memory still limits its size, but its lifetime doesn’t have to follow the call stack. We usually call this dynamically allocated storage the heap. Linux may supply it through the [heap] mapping you found in /proc/PID/maps back in the virtual-memory lesson or through other anonymous mappings. Nothing decides in advance what lives there or for how long. So we get exactly the flexibility we wanted, and it arrives with a bill attached. Handing stack space back was automatic, and handing heap space back is the program’s own job.
Where data lives is only half the question
Section titled “Where data lives is only half the question”When people follow program data, they usually ask whether it’s “on the stack” or “on the heap”. The much better next question, and the one this lesson is about, is how long that storage stays valid. A pointer will happily hold onto an address long after the object there stopped existing. The number still looks entirely convincing, even though the right to use it has quietly evaporated.
Three useful C storage categories are:
| Storage | Typical lifetime |
|---|---|
| Automatic local storage | Until execution leaves its enclosing block or function |
| Dynamically allocated storage | From successful allocation until it is released |
| Static storage | For the lifetime of the process |
Scope is about where a name can be used in the source code. Lifetime is about when the referenced object actually exists. The two often line up, and they’re really not the same concept.
Ownership asks a third question on top of those two. What part of the program is responsible for ending a dynamic allocation’s lifetime?
| Concept | Question | Example |
|---|---|---|
| Scope | Where can this name be used? | local dynamic inside main |
| Lifetime | When does the object exist? | successful malloc through free |
| Ownership | Who arranges cleanup? | main in the first example |
A pointer can stay in scope long after its allocation has died (and a helper’s local pointer can leave scope while the allocation it returned is still perfectly live). Keep those distinctions in mind when following the pointer.
Allocate, use, and release
Section titled “Allocate, use, and release”Create lifetime.c:
#include <stdio.h>#include <stdlib.h>
int main(void) { int local = 7; int *dynamic = malloc(sizeof(*dynamic)); if (dynamic == NULL) { return 1; }
*dynamic = 42; printf("local=%d dynamic=%d\n", local, *dynamic); free(dynamic); dynamic = NULL; return 0;}Compile and run it:
gcc -Wall -Wextra -O0 -g -o lifetime lifetime.c./lifetimemalloc requests storage and returns its address, or hands back NULL when the request fails. NULL represents a null pointer constant in C. A null pointer compares unequal to a pointer to any object or function; it is safe to compare and never safe to dereference. This is a language rule, not a claim about what physical memory exists at address zero. That’s what makes it usable as a failure signal: a real allocation can’t collide with it. Dereferencing dynamic with *dynamic reaches the allocated integer, and free ends the allocation’s lifetime. Assigning NULL afterward won’t repair any other copies of that pointer, and it does stop this variable advertising the old address as though it were still usable.
If an int *alias = dynamic; exists before the free, setting dynamic = NULL leaves alias still holding that stale address. The allocation is just as invalid either way.
Check the malloc and free manual for the allocation-failure and release contracts. Those contracts determine which paths need cleanup.
Modification exercise and recovery
Section titled “Modification exercise and recovery”Create one cleanup path for a program that allocates two buffers, meaning two blocks of memory set aside to hold data. Predict the behavior when the first allocation fails, and then when the second one fails instead. Every successful allocation has to be freed exactly once, and no failed result gets dereferenced at all.
Keep this exercise focused on valid ownership and cleanup paths; you don’t need to produce a use-after-free or double-free to demonstrate them. If one of your own changes triggers one, read the diagnostic, restore the last valid version, and then trace the ownership back to the first place where two paths both claimed cleanup.
Three common lifetime failures
Section titled “Three common lifetime failures”- A memory leak loses the ability or intention to free an allocation that is no longer needed.
- A use-after-free accesses storage after it has been released.
- A double-free passes an already released allocation to
freeagain without an intervening allocation that establishes a new lifetime.
Those three failures need different explanations, even though all involve allocated memory. Each represents a different broken claim about ownership and lifetime. Later tools will help us observe them properly. For now the useful move is making the claim explicit. Who owns this allocation, and at which instruction does that ownership end?
Observe the heap mapping
Section titled “Observe the heap mapping”Pause the program just before the free by temporarily adding a getchar(); after the printf, and then recompile and run it. In another terminal, find its PID and take a look:
pgrep -n lifetimegrep '\[heap\]' "/proc/$(pgrep -n lifetime)/maps"Press Enter in that first terminal to let it finish. Small allocations may well be served out of an existing heap mapping (larger ones can end up in separate anonymous mappings entirely). An allocator is a runtime manager layered on top of memory Linux supplied, and it isn’t the heap itself.
Compare the program’s output before and after the modification. Check that both allocations are released and that the paused process exits after cleanup.
If pgrep picks the wrong process, stop and use the PID printed or recorded by the shell that launched the target. And if no [heap] row appears at all, don’t assume a mapping is missing from your output, because allocator behavior and allocation size can both change where the mapping comes from. Restore the complete baseline source any time the ownership gets uncertain.
What this lets us investigate
Section titled “What this lets us investigate”You can now talk about memory bugs in terms of object lifetime and ownership, rather than treating every pointer as valid because it happens to contain a nonzero number. The final concept lesson follows programs across the boundary into the kernel.
Check your understanding
Section titled “Check your understanding”Knowledge check
Question 1 of 2
Practice complete
You've checked the key ideas. Revisit any explanation you need, then continue when it makes sense.