The stack and the heap
Automatic variables live in a stack frame that exists only while the function runs; static objects live for the whole program; heap objects live from allocation to release. The three regions have different addresses, different lifetimes and different failure modes. This lesson shows the first two and opens the third, which the next lessons take further.
Stack frames
Every call pushes a frame holding the parameters, the return address and the local variables. The addresses of locals in nested calls show the direction: on x86_64 the stack grows downward, so each deeper frame sits at a lower address. The exact values change per run and per build.
#include <stdio.h>
static void inner(int depth)
{
int local = depth;
printf("depth %d: local %p\n", depth, (void *) &local);
}
static void outer(void)
{
int local = 1;
printf("depth 1: local %p\n", (void *) &local);
inner(2);
}
int main(void)
{
int local = 0;
printf("depth 0: local %p\n", (void *) &local);
outer();
return 0;
}
gcc -Wall -Wextra frames.c -o frames
./frames
depth 0: local 0x7ffd59e13e54
depth 1: local 0x7ffd59e13e34
depth 2: local 0x7ffd59e13e14
Each frame is 32 bytes lower than the previous one in this build. The numbers are addresses of automatic objects, so copying them into a variable that outlives the call is the mistake at the end of the lesson.
Automatic and static lifetime
A local variable is created on entry and destroyed on return; a static object is placed in a different region before main starts and keeps its address. The heap is a third region, managed explicitly, and its addresses are far from both. The program prints one address from each region.
#include <stdio.h>
#include <stdlib.h>
static int global;
int main(void)
{
int local;
int *heap = malloc(sizeof *heap);
if (heap == NULL) {
return 1;
}
*heap = 7;
printf("static: %p\n", (void *) &global);
printf("local: %p\n", (void *) &local);
printf("heap: %p (%d)\n", (void *) heap, *heap);
free(heap);
return 0;
}
gcc -Wall -Wextra where.c -o where
./where
static: 0x5654b7b2d070
local: 0x7ffece664a7c
heap: 0x7f385ffeccb0 (7)
The static address is near the program code, the local one is on the stack, and the heap address is in a separate mapping. The values differ on every run because of address space randomisation; the ordering of the regions is what repeats.
The heap
malloc returns a block whose lifetime is not tied to any function. It stays valid until free is called, which is why a function can build an object and hand it to its caller. The allocation must be checked for NULL, and every successful allocation needs exactly one matching free.
#include <stdio.h>
#include <stdlib.h>
static int *make_number(int value)
{
int *number = malloc(sizeof *number);
if (number != NULL) {
*number = value;
}
return number;
}
int main(void)
{
int *a = make_number(11);
int *b = make_number(22);
if (a == NULL || b == NULL) {
free(a);
free(b);
return 1;
}
printf("%d %d\n", *a, *b);
free(a);
free(b);
return 0;
}
gcc -Wall -Wextra heap.c -o heap
./heap
11 22
The two numbers survive the return from make_number because they are not in its frame. free(NULL) is allowed, which is why the failure path can free both pointers without another test.
Returning a local address
A pointer to a local variable points into a frame that disappears when the function returns. The compiler sees the pattern and warns, because the caller would read a frame that another call may already have reused:
#include <stdio.h>
static int *bad_pointer(void)
{
int local = 5;
return &local;
}
int main(void)
{
int *p = bad_pointer();
printf("%d\n", *p);
return 0;
}
gcc -Wall -Wextra retlocal.c -o retlocal
retlocal.c: In function 'bad_pointer':
retlocal.c:7:12: warning: function returns address of local variable [-Wreturn-local-addr]
7 | return &local;
| ^~~~~~
The program is not run here because the value it would read is whatever the next call puts in that memory. The fix is one of the alternatives this lesson introduced: return the value itself, return a pointer to a static object, or allocate the object on the heap and make the caller free it.
Exercises
- Print the addresses of two locals in the same function and of two parameters. How far apart are they, and why is the answer not fixed?
- Write a function that allocates an array of five ints on the heap, fills it, and returns the pointer. Free it in main.
- Explain why returning a pointer to a static variable works but is dangerous when the caller keeps two results from the same function.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
In which direction does the stack grow on x86_64?
Answer
Downward, toward lower addresses
Each deeper call gets a lower address, which is why the depth-2 frame in the example sits below the depth-1 frame.
-
How long does an automatic local variable live?
Answer
While its function call is active
The frame exists from entry to return, so the object is destroyed when the function returns.
-
Where does a
staticfile-scope object live?Answer
In a static region for the whole program
Static storage duration keeps the object for the whole run, and its address is far from the stack and the heap.
-
What must be checked after
malloc?Answer
That the pointer is
NULLmallocreturnsNULLwhen it cannot satisfy the request, and using that pointer would be undefined. -
What does this program print?
#include <stdio.h> #include <stdlib.h> static int *make_number(int value) { int *number = malloc(sizeof *number); if (number != NULL) *number = value; return number; } int main(void) { int *a = make_number(11); int *b = make_number(22); if (a == NULL || b == NULL) { free(a); free(b); return 1; } printf("%d %d\n", *a, *b); free(a); free(b); return 0; }Answer
11 22
The two allocated numbers keep their values after the function returns and are printed before they are freed.
-
Which kind of object lives in the stack frame of a running function?
Answer
An automatic local variable
Automatic locals live in the current frame, while static objects and heap blocks live in other regions.
-
What does
free(NULL)do?Answer
It is allowed and does nothing
The standard allows
free(NULL), which lets a failure path release both pointers without an extra test. -
On x86_64, how do the addresses of automatic locals change as calls go deeper?
Answer
Each deeper frame has a lower address
The stack grows downward on x86_64, so each deeper frame sits at a lower address.
-
This program is built with
gcc -std=c17 -Wall -Wextra retlocal.c -o retlocal. What does GCC warn about?#include <stdio.h> static int *bad_pointer(void) { int local = 5; return &local; } int main(void) { int *p = bad_pointer(); printf("%d\n", *p); return 0; }Answer
function returns address of local variable [-Wreturn-local-addr]
The frame disappears on return, so GCC warns about returning the address of an object that will not exist.
-
Why is the value read through a returned local address indeterminate?
Answer
The frame may already be reused by another call
Once the function returns, the memory is no longer reserved, and the next call can overwrite it with unrelated data.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev