Ownership

Course 1 · Ch 3
Ownership
The mechanism that actually delivers on Chapter 1's promise — memory safety with no garbage collector

Chapter 1 promised memory safety without a garbage collector. This chapter is where that promise becomes concrete — ownership is Rust's single defining idea, and nearly everything else in this course builds on it.

The Three Ownership Rules

  1. Each value has exactly one owner at a time.
  2. When the owner goes out of scope, the value is dropped (its memory freed) automatically.
  3. Ownership can be transferred (moved), but a value is never implicitly duplicated.

Every example in this chapter is really just these three rules playing out in different situations.

Stack vs. Heap, Briefly

Simple fixed-size types (i32, bool, char, tuples of these) live entirely on the stack — cheap to copy, since copying just means duplicating a few fixed bytes. A String (or Vec, covered in Chapter 8) stores its actual character data on the heap, with only a pointer, length, and capacity sitting on the stack. This distinction is exactly why move semantics matter: moving a String is cheap — it's just copying that small pointer/length/capacity trio, never the underlying heap data itself.

Move Semantics

let s1 = String::from("hello"); let s2 = s1; println!("{}", s1); // compile error: value borrowed after move

Coming from Go or JavaScript, let s2 = s1 looks like it should leave both variables usable — either both reference the same underlying string, or the whole value gets copied. Rust does neither: it moves ownership of the heap data from s1 to s2, and s1 becomes invalid from that point on. Using it afterward is a compile error, not a runtime surprise.

Why Not Just Copy Everything?

The alternative — deep-copying on every assignment — would be safe, but expensive, and that cost would be invisible in the code. Rust instead makes moving the default (cheap, just a pointer/length/capacity copy) and requires an explicit .clone() call whenever a real, expensive deep copy is actually wanted — making that cost visible right in the source.

let s1 = String::from("hello"); let s2 = s1.clone(); // an explicit, real deep copy — both s1 and s2 are valid println!("{} {}", s1, s2); // fine

Simple stack-only types (i32, f64, bool, char, and tuples made entirely of these) implement Rust's Copy trait instead — copying them is so cheap that Rust just does it automatically on assignment, and neither variable becomes invalid:

let x = 5; let y = x; println!("{} {}", x, y); // fine — i32 is Copy, x was never moved

One Owner at a Time & Automatic Cleanup

When a variable goes out of scope — the end of a block or function — Rust automatically calls drop on it, freeing its memory immediately and deterministically. This is the actual mechanism that eliminates the need for a garbage collector: because ownership rules guarantee, at compile time, that exactly one thing is ever responsible for a given piece of data, the compiler always knows the exact moment cleanup should happen — no background process needs to figure it out at runtime.

Ownership and Functions

Passing a value to a function moves it in (unless the type is Copy) — the function becomes the new owner, and the caller's variable becomes invalid after the call, unless the function explicitly hands ownership back by returning the value.

fn takes_ownership(s: String) { println!("{}", s); } // s is dropped here — the function's scope ends fn main() { let s = String::from("hello"); takes_ownership(s); println!("{}", s); // compile error: s was moved into the function }

This is genuinely inconvenient if the caller just wanted the function to look at the value without giving it up entirely — which is exactly the problem Chapter 4's borrowing exists to solve.

Move TypesCopy Types
ExamplesString, Vec<T>, most custom structsi32, f64, bool, char, tuples of these
On assignmentOwnership moves; original becomes invalidValue is copied; both remain valid
To get a real duplicateExplicit .clone()Automatic — no clone needed
This is the answer to Chapter 1's question
"How does Rust get memory safety without a garbage collector?" — this chapter is the full answer: exactly one owner, enforced at compile time, with deterministic cleanup the instant that owner goes out of scope. No runtime process ever needs to guess when memory is safe to free.
"Borrow of moved value" is the most common early Rust error
Trying to use a variable after it's been moved — into another variable or into a function — is by far the most frequent compiler error newcomers hit, often described as "fighting the borrow checker." It's not a bug in your understanding; it's the compiler correctly enforcing rule #1. Chapter 4's borrowing (& and &mut) is specifically designed to solve the exact frustration this chapter's function example just demonstrated — letting a function use a value without taking ownership of it at all.

Coding Challenges

Challenge 1

Write code that creates a String, moves it into a second variable, then attempts to print the original variable. Run cargo run, note the exact compiler error, then fix it using .clone() instead so both variables remain usable.

📄 View solution
Challenge 2

Explain why let x = 5; let y = x; println!("{} {}", x, y); compiles fine with no error, while the equivalent pattern with a String does not — referencing the Copy trait specifically.

📄 View solution
Challenge 3

Write a function that takes a String, prints it, and returns it back to the caller so the caller's variable stays usable after the call — without using .clone() anywhere.

📄 View solution

Chapter 3 Quick Reference

  • Three rules: one owner at a time; drop happens automatically at scope end; a value is never implicitly duplicated
  • Move — assigning/passing a heap-backed value (String, Vec) transfers ownership; the original variable becomes invalid
  • .clone() — an explicit, real deep copy; makes the cost of copying visible in the code
  • Copy trait — simple stack-only types (i32, f64, bool, char) copy automatically on assignment; neither variable is invalidated
  • drop — called automatically when a variable's owner goes out of scope; deterministic, no garbage collector involved
  • Passing a value to a function moves it in, unless the type is Copy or the function returns it back
  • "Borrow of moved value" errors are normal and common — not a sign of doing something wrong
  • Next chapter: Borrowing & References — & and &mut, letting a function use a value without taking ownership