Skip to main content
Rust · lesson 8 of 30
Rust beginner Lesson 8 of 30

Ownership in Rust

Understand Rust's ownership model through runnable examples: the three rules, move semantics, the Copy trait, cloning, and Drop/RAII order.

By SkillByExample · Updated

The Three Ownership Rules

Ownership is the feature that makes Rust unique. It is the mechanism through which Rust achieves memory safety without a garbage collector — no reference counting, no GC pauses, no runtime overhead. The entire system is enforced at compile time through three rules:

  1. Each value in Rust has exactly one owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value is dropped (freed).

These three rules, taken together, eliminate memory leaks, use-after-free bugs, and double-frees — the most common sources of security vulnerabilities in systems software.

Scope and Drop

The most immediate consequence of ownership is deterministic memory management. When a variable goes out of scope, its value is automatically freed. There is no free() to forget, no garbage collector to wait for — the compiler inserts the cleanup code exactly where it is needed.

fn main() {
    {
        let s = String::from("hello"); // s owns the String — heap memory is allocated
        println!("{}", s);
    } // s goes out of scope here — drop(s) is called automatically
      // the heap memory is freed at this exact point

    // println!("{}", s); // ERROR: s is no longer in scope
}
$ rustc ownership_scope_and_drop.rs && ./ownership_scope_and_drop
hello

drop is called automatically by the compiler at the closing brace of the owner’s scope. This is deterministic — you always know exactly when memory is freed.

Stack vs Heap

Understanding the stack/heap distinction is essential to understanding ownership, because the two memory regions have fundamentally different characteristics that drive Rust’s design decisions.

  • Stack: Fixed-size data known at compile time. Push/pop is extremely fast. Integers, floats, booleans, chars, fixed-size arrays all live here.
  • Heap: Data whose size is unknown at compile time or may change at runtime. Allocated through an allocator, must be explicitly freed. String, Vec, Box all store their data on the heap.
use std::mem::size_of;

fn main() {
    let x = 5;                        // stored entirely on the stack
    let s = String::from("hello");    // s (a 3-word struct) is on the stack,
                                      // but the actual string bytes are on the heap
    println!("x = {}, s = {}", x, s);
    println!("size_of::<i32>()    = {} bytes", size_of::<i32>());
    println!("size_of::<String>() = {} bytes", size_of::<String>());
    println!("s.len() = {}, s.capacity() = {}", s.len(), s.capacity());
}
$ rustc ownership_stack_vs_heap.rs && ./ownership_stack_vs_heap
x = 5, s = hello
size_of::<i32>()    = 4 bytes
size_of::<String>() = 24 bytes
s.len() = 5, s.capacity() = 5

The String itself is always 24 bytes on a 64-bit target — a pointer, a length and a capacity, three machine words — no matter how long the text is. The five bytes of "hello" live on the heap at the address that pointer holds; len and capacity are both 5 because String::from allocates exactly what it needs.

Move Semantics

When you assign a heap-allocated value to another variable, ownership moves — the original binding is invalidated and the new variable becomes the sole owner. This prevents double-free errors: because there is always exactly one owner, there is always exactly one drop. Rust does not silently copy heap data — the assignment is cheap (just copies the stack-stored pointer/length/capacity) and the original becomes unusable.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // ownership moves to s2 — s1 is no longer valid

    println!("{}", s1); // ERROR: value moved to s2
    println!("{}", s2);
}
$ rustc ownership_move_semantics.rs
error[E0382]: borrow of moved value: `s1`
 --> ownership_move_semantics.rs:5:20
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1; // ownership moves to s2 — s1 is no longer valid
  |              -- value moved here
4 |
5 |     println!("{}", s1); // ERROR: value moved to s2
  |                    ^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let s2 = s1.clone(); // ownership moves to s2 — s1 is no longer valid
  |                ++++++++

The error points at three places: where s1 got its String, where it moved (let s2 = s1), and where it was used after the move. Nothing runs — the program never reaches println! because it never compiles. The fix is to stop using s1 once it has moved:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // ownership moves to s2 — s1 is no longer valid

    println!("{}", s2); // OK — s2 is the owner now
}
$ rustc ownership_move_semantics_2.rs && ./ownership_move_semantics_2
hello

The same move happens when you pass a value to a function — the function becomes the new owner:

fn take_ownership(s: String) {
    println!("got: {}", s);
} // s is the owner here, so it is dropped when this function returns

fn main() {
    let s = String::from("world");
    take_ownership(s); // s is moved into the function
    // println!("{}", s); // ERROR: s was moved — it no longer exists here
}
$ rustc ownership_move_semantics_3.rs && ./ownership_move_semantics_3
got: world

And when returning from a function, ownership moves back to the caller:

fn give_ownership() -> String {
    let s = String::from("hello");
    s // move ownership to the caller — the String is not dropped here
}

fn take_and_give_back(s: String) -> String {
    s // move the received value back to the caller
}

fn main() {
    let s1 = give_ownership();          // s1 becomes owner
    let s2 = String::from("world");
    let s3 = take_and_give_back(s2);    // s2 is moved in, s3 becomes owner of the result
    println!("{} {}", s1, s3);
}
$ rustc ownership_move_semantics_4.rs && ./ownership_move_semantics_4
hello world

This is why Rust introduces borrowing — passing ownership around for every function call is tedious. But first, understand the Copy trait.

The Copy Trait

Types that are entirely stack-allocated and trivially copyable implement the Copy trait. For these types, assignment copies the value bitwise instead of moving ownership — both the original and the new binding remain valid. This is why you can use x after let y = x when x is an integer, even though you cannot do the same with a String.

fn main() {
    let x = 5;
    let y = x; // x is COPIED (not moved) because i32 implements Copy

    println!("{} {}", x, y); // both still valid
}
$ rustc ownership_the_copy_trait.rs && ./ownership_the_copy_trait
5 5

Types that implement Copy:

  • All integer types: i8, i16, i32, i64, i128, u8, u16, u32, u64, u128, isize, usize
  • Floating-point: f32, f64
  • bool, char
  • Tuples and arrays only if all their elements are Copy
  • Shared references &T

Types that do not implement Copy because they manage heap resources:

  • String
  • Vec<T>
  • Box<T>
  • HashMap, HashSet
  • Any type containing a non-Copy field

Cloning

When you genuinely need an independent copy of a non-Copy type — rather than just a reference — use .clone(). Clone is explicit by design: it signals at the call site that a potentially expensive heap allocation is happening, so readers immediately know this is not a cheap operation.

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // deep copy — allocates new heap memory for s2's data

    println!("{} {}", s1, s2); // both valid — s1 and s2 are independent owners
}
$ rustc ownership_cloning.rs && ./ownership_cloning
hello hello

Drop and RAII

Rust implements the RAII (Resource Acquisition Is Initialization) pattern. When an owner goes out of scope, Drop::drop is called on it. This is not just about memory — it applies to any resource. File handles are closed, network sockets are released, mutex locks are unlocked, all automatically when the owning variable leaves scope.

use std::fs::{self, File};
use std::io::Write;

fn main() {
    {
        let mut f = File::create("temp.txt").unwrap(); // file is opened (resource acquired)
        writeln!(f, "written before the drop").unwrap();
    } // f is dropped here — file is flushed and closed automatically, no close() needed

    let contents = fs::read_to_string("temp.txt").unwrap();
    print!("read back: {}", contents);
    fs::remove_file("temp.txt").unwrap();
}
$ rustc ownership_drop_and_raii.rs && ./ownership_drop_and_raii
read back: written before the drop

You can implement Drop for your own types to run custom cleanup logic:

struct Resource {
    name: String,
}

impl Drop for Resource {
    fn drop(&mut self) {
        // This runs automatically when the Resource goes out of scope
        println!("Dropping resource: {}", self.name);
    }
}

fn main() {
    let _r1 = Resource { name: String::from("A") };
    {
        let _r2 = Resource { name: String::from("B") };
        println!("Inside inner block");
    } // _r2 dropped here

    println!("Back in outer block");
} // _r1 dropped here
$ rustc ownership_drop_and_raii_2.rs && ./ownership_drop_and_raii_2
Inside inner block
Dropping resource: B
Back in outer block
Dropping resource: A

B is dropped the moment its inner block closes, before “Back in outer block” prints; A is dropped last, when main returns. The leading underscore in _r1 only silences the unused-variable warning — the value still lives until the end of its scope.

When several values share one scope, they are dropped in reverse order of declaration — like a stack unwinding.

Forcing Early Drop

The standard library’s drop(value) function lets you drop a value before the end of its scope. This is useful when you need to release a resource — like a mutex lock — before the scope ends so that other code can acquire it.

fn main() {
    let s = String::from("hello");
    println!("created: {}", s);
    drop(s); // explicitly drop s before the end of the block
    // println!("{}", s); // ERROR: s has already been dropped
    println!("s has been dropped early");
}
$ rustc ownership_forcing_early_drop.rs && ./ownership_forcing_early_drop
created: hello
s has been dropped early

Ownership and Functions: Summary

fn main() {
    // Copy type — assignment copies, both bindings remain valid
    let n = 42;
    let m = n;
    println!("{} {}", n, m); // both valid

    // Non-Copy type — assignment moves, original is invalidated
    let v = vec![1, 2, 3];
    let w = v;
    // println!("{:?}", v); // ERROR: v was moved
    println!("{:?}", w);

    // Non-Copy type — clone for an explicit independent copy
    let a = String::from("hello");
    let b = a.clone();
    println!("{} {}", a, b); // both valid — independent heap allocations

    // Passing a non-Copy value to a function moves it
    let s = String::from("world");
    let len = calculate_length(s); // s is moved into calculate_length
    // s is no longer accessible here
    println!("length: {}", len);

    // Use references (borrowing) to avoid moving — see next tutorial
}

fn calculate_length(s: String) -> usize {
    s.len()
} // s is dropped here — the String is freed
$ rustc ownership_ownership_and_functions_summary.rs && ./ownership_ownership_and_functions_summary
42 42
[1, 2, 3]
hello hello
length: 5

The next tutorial covers borrowing and references, which let you use values without transferring ownership.

Practice

  1. In the Resource example, declare a third value let _r3 = Resource { name: String::from("C") }; right after _r1, in the outer scope. Predict the full output before running it.

    Solution
    Inside inner block
    Dropping resource: B
    Back in outer block
    Dropping resource: C
    Dropping resource: A

    A and C share a scope, so they drop in reverse declaration order: C (declared last) goes first, then A.

  2. Change let _r1 = Resource { ... } to let _ = Resource { ... }. What moves in the output, and why?

    Solution
    Dropping resource: A
    Inside inner block
    Dropping resource: B
    Back in outer block

    _ is not a variable name — it is a pattern that binds nothing. With no owner to hold it, the Resource is dropped at the end of that statement, before anything else prints. _r1 is a real binding that lives to the end of main.

  3. In the take_ownership example, uncomment the println!("{}", s); after the call. Which error code does the compiler report, and what two fixes does it suggest?

    Solution

    error[E0382]: borrow of moved value: \s`. The compiler suggests changing the parameter to borrow instead ("consider changing this parameter type in function take_ownershipto borrow instead if owning the value isn't necessary") and, failing that,take_ownership(s.clone()). The first fix is the one the next lesson teaches: fn take_ownership(s: &str)`.

  4. Define #[derive(Debug, Clone, Copy)] struct Point { x: i32, y: i32 }, assign let b = a;, and print both. Then add a label: String field. What happens?

    Solution

    With two i32 fields, println!("{:?} {:?}", a, b) prints Point { x: 1, y: 2 } Point { x: 1, y: 2 } — a is still usable because it was copied. Adding label: String fails to compile:

    error[E0204]: the trait `Copy` cannot be implemented for this type
    ...
    5 |     label: String,
      |     ------------- this field does not implement `Copy`

    A type can only be Copy if every field is Copy; one heap-owning field is enough to forbid it.

Frequently Asked Questions

What are the three ownership rules?
1) Each value has exactly one owner. 2) There can only be one owner at a time. 3) When the owner goes out of scope, the value is dropped.
What is a move in Rust?
Assigning a non-Copy value to a new variable transfers ownership to that variable. The original binding is no longer valid.
What types implement Copy?
All primitive scalar types (integers, floats, bool, char), tuples/arrays of Copy types, and raw pointers. Types that manage heap memory (String, Vec) do not implement Copy.