Series: Learn Rust from Scratch | Post 4 of 11
Introduction
This is the most important post in the series. Ownership is Rust's most distinctive feature — the mechanism that allows it to guarantee memory safety without a garbage collector.
If ownership feels confusing at first, that's completely normal. Every Rust programmer goes through this. Stick with it — once it clicks, you'll write code with a level of confidence you've never had before.
The Problem: Memory Management
Every program uses memory. The question is: who is responsible for freeing that memory when it's no longer needed?
- C/C++: The programmer manually allocates and frees memory. Forget to free → memory leak. Free too early → crash. Free twice → crash.
- Python/Java/Go: A garbage collector tracks what's in use and periodically cleans up. Safe, but has runtime overhead and unpredictable pauses.
- Rust: A set of compile-time rules (ownership) ensures memory is freed exactly when it's no longer needed. No runtime cost, no garbage collector, no bugs.
The Three Rules of Ownership
Rust's entire memory model is built on three rules:
- Each value in Rust has a variable called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (freed).
Scope and the Stack
fn main() {
// `x` does not exist yet
{
let x = 5; // x comes into scope
println!("x = {}", x); // x is valid here
} // x goes out of scope; memory freed automatically
// println!("{}", x); // ❌ Error: x is no longer valid
}
For simple types like integers, this is trivial. Things get interesting with heap-allocated data.
The String Type and the Heap
String literals ("hello") are stored in the program binary — they're immutable and fixed-size. The String type stores data on the heap and can be resized:
fn main() {
let s1 = String::from("hello"); // s1 owns the String on the heap
println!("s1 = {}", s1);
} // s1 goes out of scope → Rust calls `drop()` automatically → heap memory freed
Move Semantics
What happens when you assign a heap-allocated value to another variable?
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is MOVED to s2
// println!("{}", s1); // ❌ Compile error! s1 is no longer valid
println!("{}", s2); // ✅ s2 is the new owner
}
This is called a move. Unlike some languages where this would copy the data (potentially expensive), Rust moves ownership of the heap data from s1 to s2. s1 is now invalid.
Why? If both s1 and s2 owned the same heap data, they'd both try to free it when they go out of scope → double-free bug. Rust prevents this at compile time.
s1 s2
+---------+ +---------+
| ptr ───┼──(moved)───► | ptr ───┼──► "hello" (heap)
| len: 5 | | len: 5 |
| cap: 5 | | cap: 5 |
+---------+ +---------+
(invalidated)
Clone: Making an Explicit Deep Copy
If you do need two copies of the data, use .clone():
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy — creates a new heap allocation
println!("s1 = {}", s1); // ✅ s1 is still valid
println!("s2 = {}", s2); // ✅ s2 is a separate copy
}
.clone() is explicit — it signals to readers that a potentially expensive operation is happening.
Copy Types: The Stack Exception
Simple types that are stored entirely on the stack (like integers, booleans, floats, and chars) implement the Copy trait. These are copied instead of moved:
fn main() {
let x = 5;
let y = x; // x is COPIED (not moved) — integers are Copy types
println!("x = {}", x); // ✅ Still valid!
println!("y = {}", y); // ✅ Independent copy
}
Types that implement Copy:
- All integer types (
i32,u64, etc.) f32,f64boolchar- Tuples of
Copytypes (e.g.,(i32, bool))
Ownership and Functions
Passing a value to a function follows the same ownership rules as variable assignment:
fn takes_ownership(s: String) {
// s is now owned by this function
println!("Got: {}", s);
} // s is dropped here
fn makes_copy(n: i32) {
// i32 is Copy, so a copy is passed — the caller still owns theirs
println!("Got: {}", n);
}
fn main() {
let s = String::from("hello");
takes_ownership(s); // s is MOVED into the function
// println!("{}", s); // ❌ Error: s is no longer valid
let x = 5;
makes_copy(x); // x is COPIED into the function
println!("x is still: {}", x); // ✅ Still valid
}
Output:
Got: hello
Got: 5
x is still: 5
Returning values from functions also transfers ownership:
fn create_string() -> String {
let s = String::from("world");
s // Ownership is MOVED to the caller
}
fn main() {
let my_string = create_string(); // my_string receives ownership
println!("{}", my_string);
}
References and Borrowing
Moving ownership every time you call a function is cumbersome. What if a function just needs to read a value without taking ownership?
References let you refer to a value without taking ownership. This is called borrowing.
fn calculate_length(s: &String) -> usize {
// s is a reference — we borrow it, not own it
s.len()
} // s goes out of scope, but since we don't own it, nothing is dropped
fn main() {
let s = String::from("hello, world");
// Pass a reference with `&`
let length = calculate_length(&s);
// s is still valid! We only lent it out
println!("'{}' has {} characters", s, length);
}
Output:
'hello, world' has 12 characters
Mutable References
By default, references are immutable — you can read the value but not change it. Use &mut for mutable references:
fn append_exclamation(s: &mut String) {
s.push_str("!!!");
}
fn main() {
let mut greeting = String::from("Hello"); // Must be `mut`
println!("Before: {}", greeting);
append_exclamation(&mut greeting); // Pass a mutable reference
println!("After: {}", greeting);
}
Output:
Before: Hello
After: Hello!!!
The Borrowing Rules
Rust enforces two strict rules around references:
Rule 1: You can have either one mutable reference OR any number of immutable references — but not both at the same time.
Rule 2: References must always be valid (no dangling references).
Rule 1 in Action
fn main() {
let mut s = String::from("hello");
let r1 = &s; // ✅ Immutable reference
let r2 = &s; // ✅ Another immutable reference — allowed
println!("{} {}", r1, r2);
// r1 and r2 are no longer used after this point
let r3 = &mut s; // ✅ Mutable reference — r1 and r2 are gone
println!("{}", r3);
}
This fails:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // Immutable borrow
let r2 = &mut s; // ❌ Error: cannot borrow `s` as mutable because it's also borrowed as immutable
println!("{} {}", r1, r2);
}
Why this rule? If one part of code is reading a value while another part is modifying it, you can't trust what you read. Rust prevents this data race at compile time.
Rule 2: No Dangling References
// This does NOT compile:
fn dangle() -> &String { // ❌ Returns a reference to a local variable
let s = String::from("hello");
&s
} // s is dropped here — the reference would point to freed memory!
// The correct approach: return the String itself (transfer ownership)
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership is moved to the caller — no dangling reference
}
The Slice Type
Slices let you reference a contiguous portion of a collection without owning it.
String Slices (&str)
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' { // b' ' is the byte value for a space
return &s[..i]; // Slice from start to index i
}
}
&s[..] // No space found — return the whole string
}
fn main() {
let sentence = String::from("hello world");
let word = first_word(&sentence);
println!("First word: {}", word);
// String slice examples
let s = String::from("hello world");
let hello = &s[0..5]; // "hello"
let world = &s[6..11]; // "world"
let all = &s[..]; // "hello world"
println!("{} | {} | {}", hello, world, all);
}
Output:
First word: hello
hello | world | hello world
Array Slices
fn sum(numbers: &[i32]) -> i32 {
let mut total = 0;
for &n in numbers {
total += n;
}
total
}
fn main() {
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let total = sum(&arr); // Slice of the whole array
let partial = sum(&arr[0..5]); // Slice of first 5 elements
println!("Total: {}", total);
println!("Partial (first 5): {}", partial);
}
Output:
Total: 55
Partial (first 5): 15
Summary: The Mental Model
Here's a quick reference for all the concepts in this post:
| Concept | Syntax | Effect |
|---|---|---|
| Ownership | let s = String::from("hi") |
s owns the String |
| Move | let s2 = s1 |
s1 becomes invalid |
| Clone | let s2 = s1.clone() |
Both are valid, new heap copy |
| Copy | let y = x (for ints etc.) |
Both valid, stack copy |
| Immutable reference | &s |
Borrow without owning; can't modify |
| Mutable reference | &mut s |
Borrow and modify; only one at a time |
| Slice | &s[0..5] |
Reference to a portion of data |
What's Next?
In Post 5, we'll explore Structs — Rust's way of grouping related data into custom types. We'll also look at methods and how to attach behavior to your structs.
Next Post: Structs and Methods in Rust →
Part of the Learn Rust from Scratch series on CodeWithNeo


Leave a Reply