The Heap and Allocation Lifetimes
Memory with a different lifetime
Section titled “Memory with a different lifetime”Say a helper function creates some data and hands it back to the caller. The helper is finished, but the caller isn’t done with that data. Where can it live?
An ordinary local variable won’t work. In the stack frames we’ve been examining, returning from a function makes its local space available for later calls. Returning the address of a local array doesn’t somehow keep the array alive, so the caller would be left with an address it cannot safely use.
We need memory whose lifetime we can control separately from the function call. That is what dynamic allocation gives us. The heap is the pool of memory used for these allocations: a program asks for space while running, uses it for as long as the data is needed, and then releases it.
The program can also decide how much space to ask for based on what it encounters, such as the length of a file or the number of records it has to hold. The request can still fail, because dynamic memory is flexible rather than unlimited.
The name makes the heap sound like one tidy block of memory. On Linux, glibc can place
dynamic allocations in the mapping labeled [heap] or in other mappings, so
they do not have to occupy one continuous region. They still have ordinary virtual
addresses, and the CPU reads and writes them through pointers just as it does anywhere
else. The Linux allocation manual
describes where glibc gets that storage.
We’ll use the same small C programs and Ubuntu VM as the earlier sections. If you want a longer refresher on storage categories, see The Heap and Data Lifetime.
Asking for space
Section titled “Asking for space”Somebody has to keep track of which memory is available, how much was handed out, and what has since been returned. That job belongs to the allocator. In our Ubuntu C programs, the allocator comes from glibc, the GNU C library.
We only need two allocator functions for now:
malloc(bytes)requests a block of the specified size and returns a pointer to its start. It does not initialize those bytes. For the nonzero requests on this page,NULLmeans the allocation failed, so check before using the pointer.free(pointer)releases a live allocation obtained frommallocor a related function. Afterfreereturns, the program must stop accessing that allocation.
These are library calls. The allocator can answer a request from memory it already manages and ask the operating system for more when it runs short. It does not need a new system call for every little object the program creates.
The program chooses when to release each allocation. If it allocates A and then B, it can release A while B is still in use. Heap allocations do not have to end in the nested order that stack frames do.
Keep a ticket after the helper returns
Section titled “Keep a ticket after the helper returns”Let’s make the helper return one allocated integer. Allocating one integer is overkill in a real program, but it leaves nowhere for the lifetime change to hide.
Create the working directory in the Ubuntu terminal:
mkdir -p ~/underdunn-course/heap ~/underdunn-course/notescd ~/underdunn-course/heapSave this complete program as ticket.c:
#include <stdio.h>#include <stdlib.h>
static int *make_ticket(int number){ int *ticket = malloc(sizeof *ticket); if (ticket == NULL) { return NULL; }
*ticket = number; printf("helper: %p holds %d\n", (void *)ticket, *ticket); return ticket;}
int main(void){ int *saved = make_ticket(42); if (saved == NULL) { fputs("allocation failed\n", stderr); return 1; }
printf("caller: %p holds %d\n", (void *)saved, *saved); free(saved); saved = NULL; return 0;}sizeof *ticket requests enough bytes for the integer that ticket will point to.
It does not read that integer. After the allocation succeeds, *ticket = number
initializes the new storage, and return ticket passes its address back to main.
Compile and run:
gcc -std=c17 -Wall -Wextra -O0 -g ticket.c -o ticket && ./ticketBoth lines should print the same address and the value 42. Your address will differ,
but the output will have this shape:
helper: 0x5555555592a0 holds 42caller: 0x5555555592a0 holds 42The matching addresses are the useful evidence. By the time the caller prints that
second line, make_ticket has returned. Its local pointer is gone, but the allocation
is still live. The return did not copy the integer into some new allocation; main
received a copy of the existing allocation’s address.
The pointer and the allocation
Section titled “The pointer and the allocation”There are two different things to keep track of here:
- The pointer variable stores an address.
ticketbelongs to the helper’s call, andsavedbelongs tomain. In the unoptimized stack model we’ve been using, these local variables have space in their respective stack frames. - The allocated integer lives in the storage requested by
malloc. Its lifetime continues across the helper’s return and ends atfree(saved).
Calling something a “heap pointer” only describes where the pointer leads. The pointer variable itself can be local, stored in a global variable, or part of another allocation.
Follow the successful path through the program:
| Point in the program | Local pointer | Allocated integer |
|---|---|---|
| After initialization in the helper | ticket points to it |
Live, holds 42 |
| After the helper returns | saved points to it; ticket no longer exists |
Still live, holds 42 |
After free(saved) |
saved still exists; its old value is unusable |
Lifetime ended |
After saved = NULL |
saved holds a null pointer |
Already released |
The assignment to NULL gives saved a known value after cleanup. It doesn’t free
anything by itself. If we’d assigned NULL before releasing the allocation and had
no other pointer to it, we’d have lost our way to free that storage: a memory leak.
One more catch: free does not find and clear every pointer the program copied. Every
pointer to the released object must stop being used to access it. That is why we keep
track of ownership. Here, main owns the allocation after the helper returns, so
main is responsible for releasing it.
free does not promise to erase the bytes or immediately return the underlying memory
to the operating system. The allocator may keep that storage for a later request. The
release contract
still ends our use of the allocation.
Two tickets with separate lifetimes
Section titled “Two tickets with separate lifetimes”Change main so it keeps two allocations at once. Keep the helper unchanged:
- Create the first ticket with value
42, then a second with value99. Check each call for failure. If the second fails, release the first before returning. - After both calls succeed, free the first ticket and set its pointer to
NULL. - Print the second ticket’s value, then free it and set its pointer to
NULL.
Compile again with the same command. The final print should show 99. Trace
where each allocation begins and ends. Explain why returning from either helper
call doesn’t release its ticket, and why freeing the first doesn’t release the second.
Check your lifetime trace
Each successful helper call creates a separate allocation. Both remain live after
their helpers return. Freeing the first ends only that allocation’s lifetime, so the
second still holds 99 and can be read before its own cleanup.
A successful version has two malloc calls and two matching free calls. If the
second pointer is only a copy of the first, there is still just one allocation. Go
back and make a separate make_ticket(99) call.
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.
Recovery
Section titled “Recovery”If compilation fails, fix the reported line before running. The && in the command
prevents an old executable from running after a failed build. To recover the baseline,
restore the complete listing above and compile again.
Addresses will change across runs, so compare the helper and caller within one run.
If the program prints allocation failed, it exits without trying to use the failed
allocation. Restore the one-integer request and check that the VM has available memory
before trying again.
Inside the allocator
Section titled “Inside the allocator”We now know what the program asks for and when it gives that storage back. The allocator still needs somewhere to check the sizes and availability of the blocks it manages. In Anatomy of a Heap Chunk, we’ll look at how glibc represents those blocks and where it hides the bookkeeping.