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:
&Tpoints to a valid, properly alignedTthat remains alive for the reference’s lifetime;&mut Tprovides exclusive access to thatTwhile 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.
Print an address with {:p}
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
i32normally needs no heap allocation; - an owned
Stringhas 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?