Rust ownership gives each value exactly one owner at compile time — when the owner goes out of scope the value is automatically freed; assignment moves ownership rather than copying; references borrow without taking ownership; and the borrow checker verifies at compile time that all references are valid.
Rust's ownership system is the compiler checking that every value has exactly one owner at any time, and that references are always valid — eliminating use-after-free, double-free, and data races at compile time, with zero runtime cost.
Ownership rules
Rust has three ownership rules that the compiler enforces:
- Each value has exactly one owner.
- When the owner goes out of scope, the value is dropped (memory freed).
- There can only be one owner at a time — assigning moves ownership, not copies.
fn main() {
// s1 owns the String "hello" on the heap
let s1 = String::from("hello");
// Move: s1's ownership transfers to s2
// s1 is no longer valid after this line
let s2 = s1;
// println!("{}", s1); // ERROR: value borrowed after move
// Clone: deep copy — both s2 and s3 are valid
let s3 = s2.clone();
println!("{} {}", s2, s3); // "hello hello"
// Types that implement Copy are copied, not moved
// Primitives (i32, f64, bool, char) and tuples of Copy types
let x = 5;
let y = x; // x is COPIED, not moved
println!("{} {}", x, y); // both valid: "5 5"
} // s2, s3 go out of scope here — drop() is called automatically
// Memory freed without malloc/free or GCBorrowing — references
// & creates an immutable reference — you can read but not modify
fn print_length(s: &String) { // borrows s, does not take ownership
println!("Length: {}", s.len());
} // s goes out of scope here but does NOT drop the value (we don't own it)
fn main() {
let s = String::from("hello");
print_length(&s); // pass a reference — s still valid after
print_length(&s); // can borrow multiple times
println!("{}", s); // s still valid
// Mutable reference — &mut — allows modification
let mut s2 = String::from("hello");
change(&mut s2);
println!("{}", s2); // "hello, world"
}
fn change(s: &mut String) {
s.push_str(", world");
}
// THE BORROW RULES:
// 1. At any point, you can have EITHER:
// - Any number of immutable references (&T), OR
// - Exactly ONE mutable reference (&mut T)
// 2. References must always be valid (compiler enforces this)
// These two rules prevent data races at compile timeSlices
fn main() {
let s = String::from("hello world");
// String slice — a reference to part of a String
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
let all = &s[..]; // the whole string
// String literals are slices (&str)
let literal: &str = "hello"; // &str — immutable slice into static memory
// Functions should take &str to accept both String and &str
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' { return &s[0..i]; }
}
&s[..]
}
// Array slices
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..3]; // [2, 3] — type: &[i32]
println!("{:?}", slice);
}The borrow checker in action
fn main() {
let mut v = vec![1, 2, 3];
// Cannot have mutable and immutable borrow at the same time
let first = &v[0]; // immutable borrow
// v.push(4); // ERROR: mutable borrow while immutable exists
println!("{}", first); // immutable borrow used here
// After this line, first is no longer used — borrow ends (NLL)
v.push(4); // now OK — first borrow is done
println!("{:?}", v); // [1, 2, 3, 4]
// Dangling reference — impossible in Rust
// The compiler prevents returning a reference to a local variable
// fn dangle() -> &String {
// let s = String::from("hi");
// &s // ERROR: s goes out of scope at end of function
// } // would be a dangling pointer in C/C++
// Box<T> — heap allocation with single ownership
let b = Box::new(5); // 5 on the heap
println!("{}", b); // auto-dereferenced
// b is dropped here — heap memory freed automatically
// Rc<T> — reference counted, multiple owners (single-threaded)
use std::rc::Rc;
let rc1 = Rc::new(String::from("shared"));
let rc2 = Rc::clone(&rc1); // increments reference count
println!("{} (count: {})", rc1, Rc::strong_count(&rc1)); // count: 2
} // rc1 and rc2 dropped — count reaches 0 — memory freedInterior mutability: RefCell and Cell
use std::cell::RefCell;
use std::rc::Rc;
// RefCell<T> — enforces borrow rules at RUNTIME instead of compile time
// Useful when you need mutability inside an otherwise immutable context
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// borrow() — runtime immutable borrow (panics if mut borrow active)
println!("{:?}", data.borrow());
// borrow_mut() — runtime mutable borrow (panics if any borrow active)
data.borrow_mut().push(4);
println!("{:?}", data.borrow()); // [1, 2, 3, 4]
// Common pattern: Rc<RefCell<T>> for shared mutable state
let shared = Rc::new(RefCell::new(vec![1, 2]));
let shared2 = Rc::clone(&shared);
shared.borrow_mut().push(3); // mutate through one owner
println!("{:?}", shared2.borrow()); // [1, 2, 3] — seen by all owners
// For thread-safe shared mutation: Arc<Mutex<T>>
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
}unsafe or RefCell as a first resort.clone() is explicit deep copy — not automatic. In Rust, assignment of heap-allocated types (String, Vec, Box) moves ownership. To get a copy, you must call .clone() explicitly. This is intentional: it makes expensive copies visible in the code. Stack-allocated primitives that implement Copy are copied silently.&str and String are different types. String is an owned, heap-allocated, growable string. &str is a borrowed slice — a reference to string data somewhere in memory (stack, heap, or static). Function parameters should prefer &str (accepts both) over &String (only accepts String references).Non-Lexical Lifetimes (NLL) and the borrow checker algorithm
Rust's borrow checker was significantly improved in Rust 2018 with Non-Lexical Lifetimes (RFC 2094). Before NLL, a borrow lasted until the end of its lexical scope (the closing brace). With NLL, a borrow ends at the last point it is used. This eliminated many false positives where the borrow checker rejected code that was actually safe. The NLL algorithm models borrows as a region constraint problem — it computes the minimal span of code where each reference is live, then checks that no conflicting borrows overlap in their live ranges.
The ownership model and memory layout
Rust's memory model is explicit. The stack holds: local variables with known size, function call frames, and fixed-size data. The heap holds: dynamically sized data, accessed through pointers. A String is a struct of (pointer, length, capacity) on the stack pointing to data on the heap. When a String moves, the struct is bitwise-copied to the new location and the original is invalidated — no heap allocation happens. When a String is dropped, its heap memory is freed via the Drop trait, which generates the appropriate free call. There is no garbage collector — every allocation has a single, statically-known point of deallocation.