Stack vs Heap Memory Allocation
Pointers, References and Memory Layout

3.3 Stack vs Heap Memory Allocation

Variables live in different memory regions with different lifetime rules and performance characteristics. The stack versus heap choice shapes both speed and correctness in systems code.

Let's look at the differences, allocations, and hazards of these two memory zones.

The Stack: Automatic and Ultra Fast

The stack is a contiguous region of memory managed automatically by the CPU's stack pointer. When a function is called, its local variables are pushed onto the stack. When the function returns, the stack pointer moves back, and all those variables are instantly discarded.

  • Cafeteria Tray Metaphor: Stack memory is like a stack of cafeteria trays. You add trays to the top and remove them from the top (Last In, First Out). It is incredibly fast because allocating memory is just shifting a single pointer register, but you cannot request a dynamic amount of space at runtime.

The Heap: Large and Dynamic

The heap is a massive pool of memory managed by the operating system. You use the heap when you do not know how much memory you need at compile time (such as loading a file or a user variable size) or when you want variables to survive after a function exits.

To allocate memory on the heap, we use the new operator. To release that allocation, we must use the matching delete operator (which returns storage to the runtime allocator, it may stay cached in your process rather than immediately going back to the OS). For arrays allocated with new[], use delete[], mixing them is undefined behavior:

int main() {
    int* heapPtr{new int{42}};
    int* arrayPtr{new int[3]{1, 2, 3}};

    std::cout << "Value on heap: " << *heapPtr << '\n';

    delete heapPtr;      // Matches new
    delete[] arrayPtr;   // Matches new[]

    heapPtr = nullptr;
    return 0;
}
Allocating and deleting heap memory.

Memory Leaks and Dangling Pointers

Because C++ has no garbage collector, you are responsible for every byte of heap memory you request. Two common failure modes:

1. Memory Leaks

If you allocate memory on the heap and lose the pointer without calling delete, that memory stays reserved. In a loop this can exhaust available memory until the operating system terminates the process.

2. Dangling Pointers

If you call delete on a pointer, the allocator reclaims that storage (it may be reused for a future allocation in the same process). However, the pointer variable still stores that old address! If you try to dereference or use that pointer again, you trigger use-after-free undefined behavior. Always set pointers to nullptr after deleting them.

int* danger{new int{100}};
delete danger; // Memory is freed

// danger still points to the old address!
// *danger = 50; // DANGEROUS: Accessing freed memory triggers Undefined Behavior.
danger = nullptr; // Safe: Pointer is now zeroed.
Dangling pointer hazard demonstration.

Stack Overflow: Physics of the Stack

Because stack frames are stacked LIFO, the compiler must know the sizes of variables. The stack has a small, restricted size allocated by the operating system (typically between 1MB and 8MB).

If you attempt to allocate a massive static variable or trigger infinite function recursion, you run out of stack space entirely. The stack pointer pushes past the boundary line, resulting in a stack overflow crash:

void recursiveCrash() {
    int giantBuffer[100000]{}; // Consumes 400KB of stack space on each recursion!
    recursiveCrash();          // Crashing the stack after just a few calls
}
Exceeding stack bounds with recursive overflows.

Allocation Latency Benchmarks

Why not use the heap for everything? Because the stack is physically much faster. Allocating stack space is done in a fraction of a nanosecond with a single CPU instruction:

sub rsp, 16 ; Shifting the stack pointer down is instant

Heap allocation is usually more work than adjusting a stack pointer, but new does not normally make a system call for every allocation. Allocators keep per-thread caches and request pages from the OS only when necessary. Prefer automatic storage for clear lifetime and cheap small objects; profile allocation-heavy paths before changing their design.

The Physical Cost of Heap Allocation (Page Faults)

A heap allocation asks the runtime allocator for storage; its strategy may use size classes, per-thread caches, central data structures, and occasional operating-system page requests. Allocation cost and contention depend on the allocator and workload, not a single universal algorithm.

A page fault means a virtual page is not currently mapped as needed and may be satisfied from memory or, in severe cases, storage. It is not the normal cost of every allocation. Use allocation profiling to decide whether batching, an arena, or a different data layout is justified.

OS memory beyond malloc and new (optional)

The heap is one slice of your process's virtual address space. Operating systems also expose other mechanisms worth knowing about:

  • mmap / memory-mapped files: map file pages directly into virtual memory so reads and writes touch the same address space as ordinary variables. Databases, game asset loaders, and IPC buffers use this pattern. On POSIX systems see mmap(2); on Windows see CreateFileMapping.
  • NUMA (non-uniform memory access): on multi-socket machines, RAM attached to a distant socket has higher latency. Thread and allocation placement then matters; Linux tools such as numactl help experiments. Most application code ignores NUMA until profiling shows remote-memory stalls.
  • Further reading: the Linux mmap manual page and your platform's virtual-memory documentation. Lesson 3.6 covers parsing bytes once they are in memory.

Threads usually have separate stack storage, while heap allocators must coordinate shared state in some situations. That makes lifetime clarity, not a blanket speed rule, the primary reason to prefer automatic storage.

Smart Pointers (Safe Dynamic Memory)

Because manually calling new and delete is error prone, modern C++ introduces Smart Pointers (defined in the <memory> header). Smart pointers are class wrappers around raw heap pointers that automatically delete the managed memory when they exit scope using RAII.

The most common smart pointer is std::unique_ptr, which holds exclusive ownership of a heap resource:

#include <memory>
#include <iostream>

struct Asset {
    Asset() { std::cout << "Asset acquired!\n"; }
    ~Asset() { std::cout << "Asset destroyed and freed!\n"; }
};

int main() {
    {
        // Allocate heap memory safely using make_unique
        std::unique_ptr<Asset> ptr{std::make_unique<Asset>()};
        // No delete call needed! Memory is automatically freed when ptr exits scope.
    }
    return 0;
}
Safe heap allocations using std::unique_ptr.
Finished reading this lesson?