What Is a Reference
A reference (&T or &mut T) is a non-owning pointer that refers to an existing value owned by another variable. Creating a reference is called borrowing: just like borrowing a physical item in real life, you can use the value through the reference for a bounded period while the original owner retains ownership and responsibility for dropping it.
Why References Are Needed alongside Ownership
Without references, passing a non-Copy value (such as a String or Vec<T>) to a function would move ownership into that function. To let the caller keep using the value afterward, the function would have to return ownership back in a tuple on every call:
// Without references: clumsy ownership ping-pong!
fn length_by_move(s: String) -> (String, usize) {
let len = s.len();
(s, len) // Must hand ownership of `s` back to the caller
}
// With a reference (`&String`): borrows `s` without taking ownership!
fn length_by_ref(s: &String) -> usize {
s.len()
}
fn main() {
let text = String::from("Rust references");
// Pass `&text` (a reference to `text`): `text` stays owned by `main`!
let len = length_by_ref(&text);
println!("'{text}' has {len} bytes");
let (text, len2) = length_by_move(text);
println!("'{text}' still has {len2} bytes");
}
'Rust references' has 15 bytes
'Rust references' still has 15 bytes
Memory Representation of a Reference
At the machine level, a reference to a sized type (&i32, &String, &Point) is a single machine-word memory address (usize—8 bytes on 64-bit platforms, 4 bytes on 32-bit platforms) pointing to the target value:
Stack (`main`) Stack (`main`) Heap
+----------------------+ +--------------------+ +-------------------+
| s_ref: &String | | text: String | | |
| addr ──────────────+──────────────> | ptr ─────────────+─────> | "Rust references" |
+----------------------+ | len = 15 | | |
(8 bytes on 64-bit) | cap = 15 | +-------------------+
+--------------------+
(24 bytes on 64-bit)
For dynamically sized types (slices &[T] and string slices &str), a reference is a 16-byte fat pointer storing both the target memory address (ptr) and the element/byte count (len).
use std::mem::size_of;
fn main() {
let value: i64 = 42;
let reference: &i64 = &value;
println!("Value: {value}");
println!("Reference address: {reference:p}");
println!("Dereferenced (*r): {}", *reference);
println!("size_of::<&i64>(): {} bytes", size_of::<&i64>());
println!("size_of::<&[i64]>(): {} bytes (fat pointer)", size_of::<&[i64]>());
}
Value: 42
Reference address: 0x16fdff1f8
Dereferenced (*r): 42
size_of::<&i64>(): 8 bytes
size_of::<&[i64]>(): 16 bytes (fat pointer)
Core Guarantees of Every Rust Reference
While a reference compiles down to the exact same machine address as a C pointer (const T* or T*), the Rust compiler enforces four strict compile-time guarantees that raw C pointers lack:
| Guarantee | Raw Pointer (*const T / *mut T or C T*) |
Rust Reference (&T / &mut T) |
|---|---|---|
| 1. Non-Null | May be null (0x0) |
Never null (optional references use Option<&T>, which has the same 8-byte size via niche optimization) |
| 2. Always Aligned & Valid | May be misaligned, uninitialized, or forged from an arbitrary integer | Always properly aligned and points to an initialized value of type T |
| 3. Never Dangles | Can outlive the freed heap/stack memory it points to | Guaranteed by lifetime checking never to outlive the owner of the referent |
| 4. Aliasing Discipline | Unrestricted concurrent reads and writes | Obeys Aliasing XOR Mutability (any number of &T, or a single &mut T) |
The Two Kinds of References
Rust provides two reference types:
- Shared (immutable) reference (
&T): Created with&expr. Allows reading the target value, but forbids modifying it. Multiple&Treferences to the same value can exist at the same time. - Mutable (exclusive) reference (
&mut T): Created with&mut expr. Allows both reading and modifying the target value (*r = new_val), and guarantees that no other reference (&Tor&mut T) to that value is active at the same time.
fn main() {
let mut score = 100;
// 1. Borrow immutably with `&`:
let reader_1: &i32 = &score;
let reader_2: &i32 = &score;
println!("Readers see: {reader_1} and {reader_2}");
// 2. Borrow mutably with `&mut` (after `reader_1` and `reader_2` are done):
let writer: &mut i32 = &mut score;
*writer += 50;
println!("Writer updated score to: {writer}");
println!("Owner sees final score: {score}");
}
Readers see: 100 and 100
Writer updated score to: 150
Owner sees final score: 150