Home Wiki Programming & Logic Ownership in Rust: The System That Eliminates Memory Crashes Forever
Programming & Logic

Ownership in Rust: The System That Eliminates Memory Crashes Forever

The Three Rules of Ownership

The ownership system is the beating heart of Rust — the reason it is safe without a garbage collector. The rules are simple but their implications run deep:

  1. Every value in Rust has exactly one owner (one variable)
  2. A value cannot have more than one owner at the same time
  3. When the owner goes out of scope, the value is freed automatically
fn main() {
    {
        let readings = vec![23.5, 24.1, 22.8]; // readings owns this Vec
        println!("Number of readings: {}", readings.len());
    } // readings goes out of scope → memory is freed automatically

    // cannot use readings here — it no longer exists
}

In C++ you would need manual delete or free — or rely on a garbage collector that pauses the program. Rust does this automatically with zero runtime cost.

Stack vs Heap: Where Data Lives

To understand ownership, you need to know where data is stored:

Stack Heap
Very fast Slower (requires allocation)
Fixed size known at compile time Variable size at runtime
i32, f64, bool, char String, Vec<T>, HashMap
Automatic copy (Copy) Ownership transfer (Move)
// on the stack — simple copy
let a: i32 = 42;
let b = a;       // b is an independent copy — a is still valid
println!("{} {}", a, b); // works!

// on the heap — ownership transfer
let s1 = String::from("temperature_sensor");
let s2 = s1;     // ownership moved to s2 — s1 is no longer valid
// println!("{}", s1); // compile error! s1 was moved
println!("{}", s2); // works

Move Semantics: Transferring Ownership

When you assign a heap value to another variable or pass it to a function, ownership transfers — the original variable becomes invalid:

fn process_data(data: Vec<f64>) {
    println!("Processing {} readings", data.len());
    // data is freed here when it goes out of scope
}

fn main() {
    let sensor_data = vec![23.5, 24.1, 22.8, 25.0];
    process_data(sensor_data);  // ownership moved to the function

    // sensor_data is no longer valid — the compiler prevents its use
    // println!("{:?}", sensor_data); // compile error!
}

Why? Because Rust guarantees that exactly one owner frees the memory — no double free, no dangling pointers.

Data Flow in a Processing Pipeline

fn read_sensors() -> Vec<f64> {
    vec![23.5, 24.1, 22.8] // creates and returns ownership to the caller
}

fn filter_alarms(data: Vec<f64>) -> Vec<f64> {
    data.into_iter().filter(|&t| t > 24.0).collect()
}

fn log_alarms(alarms: Vec<f64>) {
    for temp in &alarms {
        println!("Alarm: {:.1}°C", temp);
    }
}

fn main() {
    let data = read_sensors();        // main owns data
    let alarms = filter_alarms(data); // ownership moved — data is invalid
    log_alarms(alarms);               // ownership moved again
}

Data flows from reader to processor to logger, each step temporarily owns the data then passes it along.

Copy and Clone: When Values Are Duplicated

Copy: Automatic Duplication for Simple Types

Small types on the stack are copied automatically:

let sensor_id: u32 = 42;
let backup_id = sensor_id; // copy — both are valid
println!("Original: {}, Copy: {}", sensor_id, backup_id);

Types that implement Copy: i32, f64, bool, char, (i32, f64) (tuples of Copy types).

Clone: Explicit Deep Copy for Complex Types

To duplicate a heap value, use .clone() — an explicit deep copy:

let original = String::from("Production Line A");
let copy = original.clone(); // deep copy — both are valid

println!("Original: {}", original);
println!("Copy: {}", copy);

Caution: .clone() copies all data — if you have a Vec with a million elements, it creates a full duplicate. Use it carefully in industrial systems where performance matters.

Ownership in Practice: Sensor Data Pipeline

Let us combine everything in a realistic example:

struct SensorReading {
    id: u32,          // Copy — on the stack
    timestamp: u64,   // Copy
    value: f64,       // Copy
    unit: String,     // Move — on the heap
}

fn create_reading(id: u32, value: f64, unit: &str) -> SensorReading {
    SensorReading {
        id,
        timestamp: 1700000000,
        value,
        unit: String::from(unit),
    }
}

fn format_reading(reading: SensorReading) -> String {
    // reading was moved here — the caller cannot use it anymore
    format!("[{}] Sensor {}: {:.1} {}",
        reading.timestamp, reading.id, reading.value, reading.unit)
}

fn main() {
    let r = create_reading(101, 23.5, "°C");
    let formatted = format_reading(r);
    // r is no longer valid — it was moved to format_reading
    println!("{}", formatted);
}

The flow is clear: each function owns its data, and when it finishes, memory is freed automatically. No leaks, no bugs.

Summary

The ownership system guarantees memory safety with three simple rules: one owner, one at a time, automatic cleanup. Simple types (numbers, booleans) are copied automatically via Copy. Complex types (String, Vec) are moved — and to duplicate them you need .clone(). This system prevents memory leaks and dangling pointers with zero runtime cost. But what if you want to share data without moving it? That is the topic of the next lesson — borrowing and references.

ownership move copy clone stack heap الملكية النقل النسخ المكدس الكومة أمان الذاكرة