8_1. References
A reference lets you access a value without owning it.
- Written as
&Tor&mut T - Does not take ownership
- Does not free memory
- Must always be valid
References are Rust’s safe alternative to raw pointers.
Why References Exist
Without references:
fn print(s: String) {
println!("{}", s);
}
let s = String::from("hello");
print(s);
// s is now gone
That’s annoying and inefficient.
With references:
fn print(s: &String) {
println!("{}", s);
}
let s = String::from("hello");
print(&s);
// s still exists
References enable safe sharing.
References and Ownership
Ownership Rule
References never own data.
let s = String::from("hello");
let r = &s;
sowns the heap allocationrjust points to it- When
sis dropped,rbecomes invalid (and Rust prevents this)
Immutable References (&T)
What they allow
- Read access
- Multiple references at the same time
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
Why this is safe
- No one can modify
s - No data races
- No invalid memory access
Mutable References (&mut T)
What they allow
- Read + write access
- Exactly one mutable reference at a time
let mut s = String::from("hello");
let r = &mut s;
r.push_str(" world");
println!("{}", r);
Why this is restricted
- Two writers = race conditions
- Writer + reader = inconsistent reads
Rust blocks this at compile time.
Immutable vs Mutable Rule (Critical)
At any moment, either:
- Many immutable references
- One mutable reference
Both at the same time
let r1 = &s;
let r2 = &mut s; // ❌ compile error
This rule is the foundation of Rust’s thread safety.
Reference Scope (Non-Lexical Lifetimes)
Rust tracks actual usage, not just braces.
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1); // last use
let r2 = &mut s; // ✅ allowed
r2.push_str("!");
Even though r1 is still in scope textually, Rust knows it’s no longer used.
References in Function Parameters
Immutable reference
fn length(s: &String) -> usize {
s.len()
}
Mutable reference
fn append(s: &mut String) {
s.push('!');
}
Usage
let mut s = String::from("hello");
let len = length(&s);
append(&mut s);
println!("{} ({})", s, len);
References vs Copy
References themselves are Copy:
let s = String::from("hello");
let r1 = &s;
let r2 = r1; // copy of reference
- Both point to the same data
- Borrowing rules still apply
- Ownership does not change
Dangling References (Prevented)
Rust forbids references that outlive data.
let r: &String;
{
let s = String::from("hello");
r = &s; // ❌ error
}
Why?
sis dropped- Heap memory freed
rwould dangle
Rust stops this at compile time.
References and the Heap
let s = String::from("hello");
let r = &s;
Memory layout:
Stack:
r ──→ s ──→ Heap("hello")
- Reference points to stack value
- Stack value points to heap
- Lifetimes guarantee everything stays valid
References and Structs
Struct Holding References
struct Excerpt<'a> {
text: &'a str,
}
Excerpt cannot outlive the data it references
Usage:
let s = String::from("hello world");
let e = Excerpt { text: &s[0..5] };
println!("{}", e.text);
References and Pattern Matching
Moving (bad)
let opt = Some(String::from("hello"));
match opt {
Some(s) => println!("{}", s), // move
None => {}
}
Borrowing (good)
match &opt {
Some(s) => println!("{}", s),
None => {}
}
Slices Are References
let s = String::from("hello world");
let slice = &s[0..5];
&stris a reference- No allocation
- No ownership transfer
Same for arrays:
let arr = [1, 2, 3];
let slice = &arr[..];
References vs Raw Pointers
| Feature | Reference | Raw Pointer |
|---|---|---|
| Always valid | ✅ | ❌ |
| Null allowed | ❌ | ✅ |
| Borrow checked | ✅ | ❌ |
| Safe by default | ✅ | ❌ |
Raw pointers exist (*const T, *mut T) but require unsafe.
Mental Model That Works
- Ownership → who frees memory
- References → who can access memory
- Borrow checker → traffic cop
- Lifetimes → how long access lasts
If the compiler allows it, the reference is guaranteed safe.