What You'll Learn
How Rust solves the billion-dollar mistake (Null Pointers and memory leaks) at compile time without a Garbage Collector, using its unique system of Ownership and Borrowing.
The Problem with C/C++ and Java
Memory management is hard. Historically, languages forced you to choose between two evils:
- Manual Memory Management (C/C++): The programmer must explicitly
malloc()andfree()memory. This leads to horrific bugs: double frees, memory leaks, and use-after-free vulnerabilities. - Garbage Collection (Java/Go/Python): A background process pauses execution to scan for unused memory and clean it up. This leads to unpredictable performance spikes and high memory overhead.
Rust offers a third way: Ownership. Memory is managed through a system of rules checked at compile time. No garbage collector, no manual freeing, and 100% memory safety.
1. The Rules of Ownership
Rust's compiler enforces three strict rules:
- Each value in Rust has a variable that's called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped (memory freed automatically).
fn main() {
let s1 = String::from("hello"); // s1 owns the memory on the heap
let s2 = s1; // Ownership is MOVED to s2.
// println!("{}", s1);
// ❌ ERROR: "value borrowed here after move"
// s1 is completely invalid now. This prevents double-free errors!
println!("{}", s2); // ✅ Works perfectly
} // s2 goes out of scope, memory is freed here.
2. Borrowing (References)
Moving ownership every time you pass a variable to a function would be exhausting. Instead, Rust allows you to borrow values using references (&).
fn main() {
let s1 = String::from("hello");
// We pass a reference (&s1) instead of moving ownership
let len = calculate_length(&s1);
// ✅ We can still use s1 here because we only borrowed it!
println!("The length of '{}' is {}.", s1, len);
}
// Function takes a reference to a String, not ownership
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but since it doesn't own the data, nothing is freed.
3. The Borrow Checker Rules
To prevent Data Races (where two pointers access data simultaneously and one modifies it), the compiler's Borrow Checker enforces a strict rule at compile time:
You can have exactly ONE mutable reference OR ANY NUMBER of immutable references, but NOT BOTH at the same time.
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
// let r2 = &mut s;
// ❌ ERROR: cannot borrow `s` as mutable more than once at a time
println!("{}", r1);
// Note: If you wanted multiple readers, that's fine!
let v = String::from("world");
let read1 = &v;
let read2 = &v; // ✅ Multiple immutable borrows are safe!
println!("{} {}", read1, read2);
}
Fighting the Borrow Checker
New Rust developers often feel like they are fighting the compiler. If your code won't compile because of lifetimes or borrowing rules, the compiler is probably saving you from a crash or security vulnerability in production. Embrace it!