References and Borrowing

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:

  1. Shared (immutable) reference (&T): Created with &expr. Allows reading the target value, but forbids modifying it. Multiple &T references to the same value can exist at the same time.
  2. 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 (&T or &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