On this page

Ownership: the heart of Rust

15 min read TextCh. 2 — Ownership and Borrowing

Ownership: Rust's central concept

The ownership system is what distinguishes Rust from all other programming languages. It is the mechanism that allows Rust to guarantee memory safety without a garbage collector (GC).

To understand why ownership exists, we first need to understand the problem it solves.

The problem: memory management

All programs must manage the memory they use. There are two main approaches:

  1. Garbage collection (GC): The language automatically tracks which memory is no longer used and frees it. Advantage: simple for the programmer. Disadvantage: unpredictable pauses, higher memory usage, not suitable for real-time systems.

  2. Manual management: The programmer is responsible for malloc/free or new/delete. Advantage: full control. Disadvantage: use-after-free, double-free, memory leaks, buffer overflows.

Rust takes a third path: the compiler verifies ownership rules at compile time and generates code that frees memory automatically at exactly the right moment, with no runtime overhead whatsoever.

The three rules of Ownership

Rust has exactly 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.

These rules sound simple but have profound implications.

Stack vs Heap

To understand ownership, you need to be clear about the difference between stack and heap:

Stack:

  • LIFO (last in, first out) memory
  • Size known at compile time
  • Instantaneous allocation and deallocation
  • Very fast
  • Examples: i32, f64, bool, char, [i32; 5]

Heap:

  • Dynamic memory, can grow or shrink
  • The OS finds a free space and returns a pointer
  • Slower than the stack
  • Requires explicit management (in C/C++) or ownership (in Rust)
  • Examples: String, Vec<T>, Box<T>
fn main() {
    // On the stack: fixed size, copied automatically
    let x: i32 = 5;    // 4 bytes on the stack
    let y = x;         // Trivial copy
    println!("x={x}, y={y}"); // Both valid
    
    // On the heap: String has 3 parts on the stack:
    // - pointer to heap (8 bytes)
    // - current length (8 bytes)
    // - capacity (8 bytes)
    // Plus the content on the heap
    let s1 = String::from("hello");
    println!("s1 points to {} bytes on the heap", s1.capacity());
}

Move semantics

When you assign a heap value to another variable, Rust performs a move, not a copy:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 is MOVED to s2
    
    // At this point:
    // - s2 is the owner of the String "hello"
    // - s1 is NO LONGER valid — the compiler knows this
    
    println!("{s2}"); // OK
    // println!("{s1}"); // Error: "value borrowed here after move"
}

Why not just copy? If Rust automatically copied, we would have two owners of the same heap data. When both went out of scope, the same memory would be freed twice — a "double free" error that causes undefined behavior.

Instead of copying, Rust invalidates s1 when it is moved to s2. Only s2 frees the memory when it goes out of scope.

Clone: explicit heap copy

When you genuinely need a full copy of a heap value, use .clone():

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone(); // Deep copy — explicit
    
    println!("s1 = {s1}"); // OK: s1 is still valid
    println!("s2 = {s2}"); // OK: s2 is an independent copy
    
    // Modifying s2 does not affect s1
    let mut s3 = s1.clone();
    s3.push_str(" world");
    println!("s1 = {s1}");  // "hello"
    println!("s3 = {s3}");  // "hello world"
}

The key point: .clone() is expensive and explicit. When you see it in code, you know a heap copy is happening.

The Copy trait

Types that are "cheap to copy" implement the Copy trait. For these types, assignment always creates a copy rather than a move:

fn main() {
    // Copy types: assignment copies, not moves
    let a: i32 = 10;
    let b = a;      // Copy
    let c: f64 = 3.14;
    let d = c;      // Copy
    let e: bool = true;
    let f = e;      // Copy
    let g: char = 'Z';
    let h = g;      // Copy
    
    // All still valid
    println!("{a} {b} {c} {d} {e} {f} {g} {h}");
    
    // References &T are Copy (the reference is copied, not the data)
    let s = String::from("text");
    let r1: &String = &s;
    let r2 = r1; // Copies the reference
    println!("{r1} {r2}"); // Both valid
}

A type can implement Copy only if all of its fields also implement Copy. String cannot be Copy because it manages heap memory.

Ownership and functions

Ownership follows the same rules when passing values to functions:

fn main() {
    let s = String::from("Rust");
    
    // Passing s MOVES ownership to the function
    print_string(s);
    // println!("{s}"); // Error: s was moved
    
    // To keep ownership, the function can return it
    let s2 = String::from("Ownership");
    let s2 = return_ownership(s2);
    println!("Got it back: {s2}");
    
    // This is verbose — references (next lesson) solve this elegantly
}

fn print_string(string: String) {
    println!("{string}");
} // string is dropped here

fn return_ownership(string: String) -> String {
    println!("{string}");
    string // Returns ownership to the caller
}

Drop: automatic memory deallocation

When an owner goes out of scope, Rust automatically calls the value's destructor, implemented through the Drop trait:

struct ImportantResource {
    name: String,
}

impl Drop for ImportantResource {
    fn drop(&mut self) {
        println!("Releasing resource: {}", self.name);
    }
}

fn main() {
    let r1 = ImportantResource { name: String::from("DB Connection") };
    let r2 = ImportantResource { name: String::from("Log file") };
    
    println!("Resources created");
    
    // When they go out of scope, Rust automatically calls drop()
    // in reverse order of creation: r2 first, then r1
    println!("Ending main...");
} // r2.drop() then r1.drop() — no programmer intervention needed

This pattern, called RAII (Resource Acquisition Is Initialization), guarantees resources are always released, even if an error condition occurs. It is impossible to forget to close a database connection in Rust.


The ownership system can feel restrictive at first, but it is the foundation of all of Rust's safety guarantees. The next lesson teaches you references and borrowing — the elegant way to use data without transferring ownership.

The Copy trait
Types that implement Copy are copied automatically instead of being moved. These are: all integers (i32, u64...), floats (f32, f64), bool, char, references (&T), and tuples/arrays of Copy types. Types with heap data (String, Vec) are NOT Copy.
Drop and the destructor
When an owner goes out of scope, Rust automatically calls drop(), which frees the heap memory. This guarantees there will never be memory leaks in safe Rust code. You do not need free() or delete().
rust
fn main() {
    // String on the heap — has an owner
    let s1 = String::from("hello");

    // MOVE: s1 is moved to s2, s1 is no longer valid
    let s2 = s1;
    // println!("{s1}"); // Error: value moved

    println!("s2 = {s2}");

    // CLONE: deep copy of the heap
    let s3 = s2.clone();
    println!("s2 = {s2}, s3 = {s3}");

    // Copy types live on the stack: copied automatically
    let x: i32 = 5;
    let y = x;  // Copy, not move
    println!("x = {x}, y = {y}"); // Both valid

    // Ownership and functions
    let string = String::from("world");
    take_ownership(string);
    // println!("{string}"); // Error: moved

    let number = 42_i32;
    make_copy(number);
    println!("number is still {number}"); // Valid: i32 is Copy
}

fn take_ownership(s: String) {
    println!("I took ownership of: {s}");
} // s is dropped here

fn make_copy(n: i32) {
    println!("I have a copy: {n}");
}