Ownership · about 40 min

The Stack and the Heap

Why this matters

Ownership, the idea at the center of Rust, is a set of rules about memory: who is responsible for freeing a piece of memory, and when. Those rules only make sense once you can picture where values actually live. Languages like Python and Java hide this behind a garbage collector; C exposes it and leaves you to get it right by hand. Rust exposes it and has the compiler check that you got it right. This lesson builds the mental picture; the next four use it constantly.

A process's memory

When your program runs, the operating system gives it a range of addresses. Simplified:

high addresses
 +---------------------------+
 |  stack                    |  grows downward: one frame per active function call
 |            |              |
 |            v              |
 |                           |
 |            ^              |
 |            |              |
 |  heap                     |  grows as the program asks for memory
 +---------------------------+
 |  static data              |  string literals, constants, global variables
 +---------------------------+
 |  code                     |  the machine instructions
 +---------------------------+
low addresses

The stack

You met stack frames in module 2. Each function call pushes a frame with space for its parameters and local variables; returning pops it. The CPU keeps a stack pointer register pointing at the top, so creating a frame is just subtracting a number from that register. Nothing could be faster.

fn main() { let a = 1; let b = add(a, 2); }      while add runs:
fn add(x: i32, y: i32) -> i32 { x + y }
                                                 +------------------+
                                                 | add   x = 1      |  <- stack pointer
                                                 |       y = 2      |
                                                 +------------------+
                                                 | main  a = 1      |
                                                 |       b = ?      |
                                                 +------------------+

The stack has two strict properties:

  1. Every frame's layout is fixed at compile time. The compiler must know the size of every local variable to compute offsets like "b is 4 bytes above the frame start". So stack values must have a size known at compile time: integers, floats, char, bool, tuples and arrays of those.
  2. A frame dies when its function returns. Its memory is immediately reused by the next call. Anything that must outlive the call cannot live in the frame.

It is also small: 8 MB for the main thread on a typical Linux system. A local array of a hundred million bytes crashes with a "stack overflow".

The heap

The heap is a large pool of memory managed by an allocator (part of the standard library, on top of the operating system). Your program can ask it, at run time, for a block of any size:

  1. Allocate: "give me 11 bytes". The allocator finds a free block and returns its address.
  2. Use the block through that address, a pointer.
  3. Free (deallocate): "I am done with the block at this address". It becomes available again.

The heap solves both stack limitations: sizes can be decided at run time, and a block lives until it is freed, regardless of which function calls come and go. The costs: allocation takes real work (finding a suitable free block), and access goes through a pointer.

A String: both at once

String is Rust's growable, heap-allocated text type, and it is the perfect example:

fn main() {
    let s = String::from("hello");
    println!("{s}: len {}, capacity {}", s.len(), s.capacity());
    println!("String value itself: {} bytes", std::mem::size_of::<String>());
    println!("heap buffer at {:p}", s.as_ptr());
}
hello: len 5, capacity 5
String value itself: 24 bytes
heap buffer at 0x55fbeccd5b10

(The address will differ on every run; {:p} prints a pointer.) In memory:

        stack (main's frame)                     heap
       +----------------------+          +---+---+---+---+---+
   s   | ptr      -------------------->  | h | e | l | l | o |
       | len      5           |          +---+---+---+---+---+
       | capacity 5           |
       +----------------------+
         24 bytes, fixed size              5 bytes, size decided at run time

The String value on the stack is three machine words (8 bytes each on 64-bit): a pointer to the heap buffer, the length (bytes in use) and the capacity (bytes allocated). Its size is fixed, so it fits in a stack frame; the text itself, whose size is not known at compile time, is on the heap.

When you append beyond the capacity, the String allocates a bigger buffer (usually double), copies the bytes over, frees the old buffer and updates its pointer:

fn main() {
    let mut s = String::new();
    println!("len {:2} cap {:2}", s.len(), s.capacity());
    for word in ["Rust", " is", " a", " systems", " language"] {
        s.push_str(word);
        println!("len {:2} cap {:2}  {s:?}", s.len(), s.capacity());
    }
}
len  0 cap  0
len  4 cap  8  "Rust"
len  7 cap  8  "Rust is"
len  9 cap 16  "Rust is a"
len 17 cap 32  "Rust is a systems"
len 26 cap 32  "Rust is a systems language"

An empty String allocates nothing (capacity 0). Doubling means that appending many times costs only a few reallocations overall.

The central question: who frees the heap block?

Stack memory takes care of itself: frames vanish on return. Heap memory does not. Something must free every block exactly once, after its last use. Get this wrong and you have one of the classic bugs:

bugwhat happens
memory leaka block is never freed; the program's memory grows until it is killed
double freea block is freed twice; the allocator's bookkeeping is corrupted, often exploitable
use after freea block is used after being freed; you read or overwrite someone else's data
dangling pointera pointer to memory that is no longer valid, waiting to cause one of the above

Languages answer the question in three ways:

The rest of this module explains how ownership works. The important part to take from this lesson is the picture: a small fixed-size handle on the stack, owning a buffer on the heap. String is one such type; Vec (a growable list) and Box (a single heap value) are others.

Common mistakes

Thinking size_of::<String>() depends on the text. It is always 24 bytes on a 64-bit machine; it measures the handle, not the heap buffer.

Thinking string literals are on the heap. "hello" is in the executable's static data. A literal has type &str, a pointer and a length pointing into that static data. String::from("hello") copies it to the heap.

Huge arrays as locals. let big = [0u8; 100_000_000]; overflows the stack:

thread 'main' has overflowed its stack
fatal runtime error: stack overflow

Large or variable-size data belongs on the heap, in a Vec (module 9) or Box (module 13).

Believing the heap is slow. Allocation costs something; access does not, beyond following one pointer. Allocating inside a tight loop is the thing to avoid, not the heap in general.

Recap

Going further: run the growth example with push of single characters in a loop of 100 and print the capacity only when it changes. Then read the first section of the Rust Book's chapter 4 ("What Is Ownership?") for a second explanation of the same picture.

Exercises

Exercise 1 · quiz

Stack or heap?

In fn main() { let n: i32 = 7; let s = String::from("hello"); }, where are the bytes of the text hello stored?

Answer
  • On the heap; main's frame holds only a pointer, a length and a capacity

A String is a small fixed-size struct of three machine words (pointer, length, capacity) that lives on the stack, plus a heap buffer holding the actual bytes. The literal "hello" is indeed stored inside the executable, but String::from copies it into a freshly allocated heap buffer so the String can own and grow it.

Exercise 2 · write code

Measure it

Print the stack size of these types with std::mem::size_of, exactly in this format:

i32: 4
f64: 8
[u8; 100]: 100
String: 24
&str: 16

Use std::mem::size_of::<T>() for each; do not type the numbers.

File: main.rs

Starting point

use std::mem::size_of;

fn main() {
    println!("i32: {}", size_of::<i32>());
}

Example

expected output

i32: 4
f64: 8
[u8; 100]: 100
String: 24
&str: 16
Hint 1

size_of::<String>() measures the stack part only: no matter how long the text, it is three words.

Reference solution
use std::mem::size_of;

fn main() {
    println!("i32: {}", size_of::<i32>());
    println!("f64: {}", size_of::<f64>());
    println!("[u8; 100]: {}", size_of::<[u8; 100]>());
    println!("String: {}", size_of::<String>());
    println!("&str: {}", size_of::<&str>());
}

Exercise 3 · write code

Watch a String grow

Read words from stdin, one per line. Start with let mut s = String::new(); and for each word, append it with s.push_str(word). After each append, print the length, and print grew whenever the capacity changed since the previous line (that is when a new, larger heap buffer was allocated). Format: the length, then grew if the capacity changed:

3 grew
6
11 grew

for the words abc, def, ghijk. Hint: keep the previous capacity in a variable; s.capacity() returns the current one. (Growth policy is an implementation detail; the expected output here matches Rust's actual policy: start at 8 bytes, then double as needed.)

File: main.rs

Starting point

use std::io;

fn main() {
    let mut s = String::new();
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let word = line.trim();
    }
}

Example

input

abc
def
ghijk

expected output

3 grew
6
11 grew

Example

input

a
b
c
d
e
f
g
h
i

expected output

1 grew
2
3
4
5
6
7
8
9 grew
Hint 1

Before the loop: let mut cap = s.capacity(); (it is 0 for an empty String).

Hint 2

After push_str, compare s.capacity() with cap, print, and update cap.

Reference solution
use std::io;

fn main() {
    let mut s = String::new();
    let mut cap = s.capacity();
    for line in io::stdin().lines() {
        let line = line.unwrap();
        let word = line.trim();
        s.push_str(word);
        if s.capacity() != cap {
            println!("{} grew", s.len());
            cap = s.capacity();
        } else {
            println!("{}", s.len());
        }
    }
}

Exercise 4 · quiz

Why not put everything on the stack?

Which are real reasons data goes on the heap instead of the stack? Select all that apply.

Answer
  • Its size is not known at compile time (text read from input, a list that grows)

  • It must outlive the function call that created it

  • It is very large; stacks are typically limited to a few megabytes

Stack frames have a fixed layout decided at compile time and vanish when the function returns, and the stack itself is small (8 MB by default for the main thread on Linux). Heap memory can be requested at any size at run time and lives until it is explicitly freed. Heap memory is not faster: allocating costs more than bumping the stack pointer, and reading it means following a pointer. Once allocated, both are ordinary RAM.