Skip to content
Courses

The Heap and Data Lifetime

How Programs WorkLesson 6 of 775 minutesLessonBuilds on: Function Calls and the StackNo lab next

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.

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:

Terminal window
gcc -Wall -Wextra -O0 -g -o lifetime lifetime.c
./lifetime

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

Scope, ownership, and allocation lifetime are separate timelines
function beginsmallocfreefunction ends
pointer name scopename can be used here
allocation lifetimestorage is valid only between malloc and free
ownershipthe owner must arrange exactly one release

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.

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.

  • 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 free again 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?

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:

Terminal window
pgrep -n lifetime
grep '\[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.

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.

Knowledge check

Question 1 of 2

A helper allocates memory with malloc and returns its pointer to the caller. What happens to the allocation?