Stack, heap, and the lifetime of data
Pointers and memory12 min readLesson 5 of 6
5.1 One stack, one frame per call
Local variables with automatic storage live in a stack frame created when the function is called and destroyed when it returns. Parameters often start in registers and may be copied into the frame.
The frame also holds the return address and saved registers. Its size is fixed when the function compiles, apart from platform extensions such as alloca, which are best left alone.
Lifetime inside the frame is short. An address that points into the frame becomes invalid the moment the function returns, even though the old bytes may still hold the old value.
#include <stdio.h>
int main(void)
{
int outer = 1;
{
int inner = 2;
printf("inner: %d\n", inner);
}
printf("outer: %d\n", outer);
return 0;
}Output
inner: 2 outer: 1
5.2 Returning an address of a local
int *bad(void) { int x = 5; return &x; } compiles with a warning on most compilers, and the pointer is invalid at the call site. The next call may reuse the frame.
The compiler cannot catch every form. The address can travel through another function or a struct before it is read. GCC and Clang report the direct case with -Wreturn-local-addr; MSVC reports it with /W4.
Return the value instead of the address, or allocate storage whose lifetime outlives the call.
#include <stdio.h>
struct Pair {
int first;
int second;
};
static struct Pair make_pair(int a, int b)
{
struct Pair result = {a, b};
return result;
}
int main(void)
{
struct Pair p = make_pair(3, 4);
printf("%d %d\n", p.first, p.second);
return 0;
}Output
3 4
5.3 Heap lifetime is manual
Heap memory lives from the moment an allocation returns until the matching free. Nothing in the language ends its life automatically, and leaving a function changes nothing.
That property makes the heap the right choice when a value must outlive the call that created it. It also means every allocated block needs a plan for release.
A pointer in a local variable can point at the heap and disappear when the variable goes out of scope. The block stays allocated, and if no other pointer holds its address, the program can never free it.
#include <stdio.h>
#include <stdlib.h>
static int *make_counter(int start)
{
int *counter = malloc(sizeof *counter);
if (counter == NULL) return NULL;
*counter = start;
return counter;
}
int main(void)
{
int *counter = make_counter(40);
if (counter == NULL) return 1;
*counter += 2;
printf("counter: %d\n", *counter);
free(counter);
return 0;
}Output
counter: 42
5.4 Dangling pointers
A dangling pointer points at memory that has been freed or at a frame that has returned. The pointer value is still there, and comparing it with NULL succeeds, so a guard does not help.
The symptoms vary. The value looks correct in a small program, changes after an unrelated call, or crashes when the allocator reuses the block.
Setting a pointer to NULL after free prevents a second free and makes a later dereference crash close to the bug. It cannot help when another copy of the same address still exists.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *data = malloc(2 * sizeof *data);
if (data == NULL) return 1;
data[0] = 5;
free(data);
data = NULL;
int *replacement = malloc(2 * sizeof *replacement);
if (replacement == NULL) return 1;
replacement[0] = 11;
printf("replacement: %d\n", replacement[0]);
printf("old pointer cleared: %d\n", data == NULL);
free(replacement);
return 0;
}Output
replacement: 11 old pointer cleared: 1
5.5 What a leak looks like
A leak is an allocation with no remaining pointer to it. The program stays correct, and the memory stays reserved until the process ends. A long-running service loses more memory with every leak until an allocation fails.
Leaks usually appear on error paths. Two allocations succeed, the third fails, and the function returns without freeing the first two. They also appear when a pointer is overwritten with a new allocation before the old one is freed.
Valgrind reports leaks with the allocation stack, AddressSanitizer does the same on Windows, Linux, and macOS, and Visual Studio has a CRT leak report for debug builds.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
char *a = calloc(8, 1);
char *b = calloc(8, 1);
if (a == NULL || b == NULL) {
free(a);
free(b);
return 1;
}
for (int i = 0; i < 7; i++) {
a[i] = 'x';
b[i] = 'y';
}
printf("%s %s\n", a, b);
free(a);
free(b);
return 0;
}Output
xxxxxxx yyyyyyy
5.6 Stack or heap
Use automatic storage first. It costs nothing to allocate, needs no cleanup, and stays local to the function. Arrays with a compile-time size belong there.
Use the heap when the size is decided at run time, when the value must outlive the function, or when the object is too large for the stack. A multi-megabyte buffer on an 8 MB Windows stack can overflow before the first byte is written.
Recursion reaches the same limit from another direction. Every call adds a frame, and deep recursion can exhaust the stack without allocating anything explicitly.
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int local[4] = {1, 2, 3, 4};
int count = 100000;
long *large = malloc((size_t)count * sizeof *large);
if (large == NULL) return 1;
for (int i = 0; i < count; i++) large[i] = i;
printf("local sum: %d\n", local[0] + local[3]);
printf("large last: %ld\n", large[count - 1]);
free(large);
return 0;
}Output
local sum: 5 large last: 99999
5.7 A lifetime checklist
Before returning a pointer, ask what it points at and how long that storage lasts. Before storing a pointer in a struct, ask the same question, because the struct may live longer than the data.
When a struct holds a pointer, the struct does not own the data by default. Either the struct owns it and its cleanup function frees it, or it borrows the data and the caller keeps the lifetime guarantee.
Nothing in C enforces the rule, so write it next to the declaration and keep allocation and release in the same layer when you can.
#include <stdio.h>
struct View {
const char *text;
int length;
};
static void print_view(const struct View *view)
{
printf("%.*s (%d)\n", view->length, view->text, view->length);
}
int main(void)
{
struct View view = {"borrowed text", 8};
print_view(&view);
return 0;
}Output
borrowed (8)
