Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Memory

Ownership describes who is responsible for a value. Stack and heap describe where a program commonly stores the value’s data. Pointers and references describe how code reaches data stored somewhere else.

These ideas are related, but they are not interchangeable:

  • a value can contain both stack data and heap data;
  • moving a value does not necessarily move its heap allocation;
  • a reference points to a value but does not own it;
  • using the heap does not automatically make a type shared or long-lived.

Function calls create stack frames

Each function call gets a stack frame containing information needed for that call. Local variables whose size is known at compile time can be stored directly in the frame.

fn add(left: i32, right: i32) -> i32 {
    let sum = left + right;
    sum
}

fn main() {
    let answer = add(20, 22);
    println!("{answer}");
}

A simplified picture while add is running looks like this:

stack
┌──────────────────────┐
│ add                   │
│ left: 20              │
│ right: 22             │
│ sum: 42               │
├──────────────────────┤
│ main                  │
│ answer: not set yet   │
└──────────────────────┘

When add returns, its frame is removed as a unit. This is fast because the program only needs to adjust the current end of the stack.

The diagram is a useful model, not a promise about exact machine layout. The optimizer may keep values in registers, remove variables, or reorganize code.

The heap stores dynamically sized allocations

The heap supports allocations whose size or lifetime does not fit neatly into one function’s fixed stack frame. The program asks an allocator for a region and later returns that region.

String is a common example:

fn main() {
    let message = String::from("hello");
    println!("{message}");
}

The String value is a small, fixed-size handle. Conceptually, it contains a pointer, a length, and a capacity. The UTF-8 bytes are stored in a heap allocation:

stack                              heap
┌──────────────────────┐          ┌─────────────┐
│ message              │          │ h e l l o   │
│ pointer ──────────────┼─────────▶│             │
│ length: 5            │          └─────────────┘
│ capacity: 5          │
└──────────────────────┘

When message is dropped, its destructor uses the pointer and capacity to release the heap allocation.

Vec<T> has the same broad shape: a fixed-size handle points to a heap region containing its elements.

A move usually moves the handle

Moving a String copies its pointer, length, and capacity into a new binding. It does not copy each byte in the heap allocation:

fn main() {
    let first = String::from("hello");
    let second = first;

    println!("{second}");
}

After the move:

stack                              heap
┌──────────────────────┐          ┌─────────────┐
│ first: unusable      │          │ h e l l o   │
├──────────────────────┤          │             │
│ second               │          └─────────────┘
│ pointer ──────────────┼─────────▶
│ length: 5            │
│ capacity: 5          │
└──────────────────────┘

Only second remains responsible for the allocation. This is why moves are often cheap even for values that manage large amounts of heap data.

By contrast, cloning the String creates a second heap allocation and copies the bytes:

#![allow(unused)]
fn main() {
let first = String::from("hello");
let second = first.clone();
}

Now each String owns and eventually frees its own allocation.

A reference is a non-owning pointer

&T is a shared reference to a T. It contains the location of a value but does not own that value:

fn main() {
    let number = 42;
    let reference = &number;

    println!("number: {number}");
    println!("through reference: {}", *reference);
}

&number creates the reference. *reference follows, or dereferences, it to access the i32.

stack
┌────────────────────────┐
│ number: 42             │◀────┐
├────────────────────────┤     │
│ reference: address ────┼─────┘
└────────────────────────┘

Rust often inserts dereferencing automatically for method calls and some expressions. Explicit * is still useful for understanding what the reference means.

A reference is valid only while its target remains valid. Lifetimes are the compiler’s way of checking that relationship:

#![allow(unused)]
fn main() {
fn invalid_reference() -> &String {
    let message = String::from("hello");
    &message
}
}

message would be dropped when the function returns, so returning a reference to it would create a dangling pointer. Rust rejects the function.

Shared and mutable references carry guarantees

References are more than memory addresses. A valid Rust reference carries rules the compiler and optimizer can rely on:

  • &T points to a valid, properly aligned T that remains alive for the reference’s lifetime;
  • &mut T provides exclusive access to that T while the mutable reference is in use;
  • neither kind of reference may be null or dangling.

These guarantees are why references participate in the borrowing rules. They also allow ordinary reference use to remain safe: dereferencing a valid Rust reference does not require an unsafe block.

Some references contain extra metadata

A reference to a sized value, such as &i32, is typically one machine word: an address.

A slice reference must also record how many elements are visible:

&[T] = data pointer + length
&str = data pointer + byte length

Such a reference is often called a fat pointer because it contains an address plus metadata.

fn main() {
    let numbers = [10, 20, 30, 40];
    let middle = &numbers[1..3];

    println!("length: {}", middle.len());
    println!("values: {middle:?}");
}

middle does not own or copy the values 20 and 30. It stores where that region begins and that it contains two elements.

Trait-object references such as &dyn Display also contain metadata, but use a table of methods instead of a length.

Pointer formatting can make a reference visible during an experiment:

fn main() {
    let value = 42;
    let reference = &value;

    println!("address: {reference:p}");
}

The exact address changes between runs and is rarely meaningful by itself. It is useful for confirming that two references point to the same value or that a clone owns a different allocation.

Do not build program logic around printed addresses.

Raw pointers provide fewer guarantees

Rust also has raw pointer types:

*const T  raw pointer for reading
*mut T    raw pointer that may permit writing

Creating a raw pointer is safe:

fn main() {
    let value = 42;
    let pointer: *const i32 = &value;

    println!("{pointer:p}");
}

Dereferencing one requires unsafe because the compiler does not prove that a raw pointer is non-null, aligned, alive, or properly synchronized. Raw pointers are mainly used at foreign-function boundaries and inside low-level data structures. Ordinary Rust code should prefer references.

Owning pointers are values with ownership behavior

Box<T> is an owning pointer. It places a T on the heap and owns that allocation:

fn main() {
    let boxed = Box::new(42);
    println!("{}", *boxed);
}

When boxed is dropped, the i32 and its heap allocation are dropped. Moving the Box transfers that ownership. Borrowing the Box or its contents lends temporary access without transferring it.

Other pointer-like types add different ownership rules. Rc<T> and Arc<T> provide shared ownership; the later concurrency chapter covers Arc<T> in detail.

Stack versus heap does not decide ownership

Avoid rules such as “owned values live on the heap” or “copied values live on the stack.” They do not hold:

  • an owned i32 normally needs no heap allocation;
  • an owned String has a stack-sized handle and heap-allocated contents;
  • a Box<i32> owns a heap allocation;
  • a reference can point to data on the stack, the heap, or static memory;
  • whether a type moves or copies is determined by Copy, not by its location.

Start with ownership: who must eventually clean up the value? Then consider representation: is the data stored inline, behind a pointer, or across both?