Rust HeapMove SemanticsownershipRustStack

Article & Tutorial

Rust Ownership Explained: Stack, Heap, Move, Copy, Clone and Drop

Learn Rust ownership step by step with stack and heap concepts, moves, copies, clones, scope, function ownership, return values and Drop behavior.

Rust Ownership Explained: Stack, Heap, Move, Copy, Clone and Drop article illustration
Rust Ownership Explained: Stack, Heap, Move, Copy, Clone and Drop article illustration
Contents

Ownership is the concept that makes Rust fundamentally different from many other programming languages. It is how Rust manages memory safely without requiring a garbage collector.

At first, ownership can feel strict. After you understand the rules, however, many Rust compiler errors start to make sense because the compiler is preventing invalid memory access before the program runs.

What is ownership in Rust?

Rust uses a set of compiler-enforced ownership rules to determine when values are valid, who is responsible for them, and when their memory can be released.

The three core ownership rules are:

  1. Every value in Rust has an owner.
  2. There can be only one owner at a time.
  3. When the owner goes out of scope, the value is dropped.

These rules are checked at compile time. They do not add a garbage collector to your running application.

Stack vs heap

Understanding the stack and heap makes ownership easier to visualize.

StackHeap
Stores values with known fixed sizeStores dynamically sized data
Very fast push/pop accessRequires allocation and a pointer
Examples: many integers, booleans, tuples of fixed-size valuesExamples: dynamically allocated String contents and Vec buffers
bash
let age: i32 = 35; let name = String::from("Melochip"); Stack Heap +-------------------+ +----------------+ | age = 35 | | H y a k i n g | | name.ptr ---------+-------->| | | name.len = 7 | +----------------+ | name.capacity = 7 | +-------------------+

The String metadata lives on the stack while its character buffer is allocated on the heap.

Scope and automatic cleanup

bash
fn main() { { let message = String::from("hello"); println!("{message}"); } // message goes out of scope and Rust releases its heap allocation }

When message leaves scope, Rust automatically runs cleanup code. This is deterministic: the compiler knows exactly when that owner is no longer valid.

Move semantics

Consider this code:

bash
fn main() { let s1 = String::from("Rust"); let s2 = s1; println!("{s2}"); }

For a heap-owning type such as String, assigning s1 to s2 normally moves ownership. Rust does not perform an implicit deep copy of the heap allocation.

bash
Before move s1 ──────> heap "Rust" After: let s2 = s1; s1 invalid s2 ──────> heap "Rust"

If you try to use s1 after the move, the compiler rejects the program:

bash
let s1 = String::from("Rust"); let s2 = s1; println!("{s1}"); // error: value borrowed here after move

This rule prevents two owners from independently trying to free the same heap allocation.

Why integers often behave differently: Copy

bash
fn main() { let x = 10; let y = x; println!("x={x}, y={y}"); }

This works because types such as many integers implement the Copy trait. Their entire value can be copied cheaply.

Common Copy examples include many scalar values such as integers, floating-point values, bool and char, plus compound types whose components are also Copy.

Clone: make an explicit deep copy

If you really want independent heap-backed values, use clone():

bash
fn main() { let s1 = String::from("Rust"); let s2 = s1.clone(); println!("s1={s1}"); println!("s2={s2}"); }

Unlike a move, cloning can allocate and copy data. Use it intentionally rather than adding .clone() everywhere just to silence ownership errors.

Ownership and functions

Passing a value to a function follows the same move/copy rules as assignment.

bash
fn consume(text: String) { println!("{text}"); } fn main() { let message = String::from("owned value"); consume(message); // println!("{message}"); // would fail: message was moved }

For Copy values:

bash
fn show_number(n: i32) { println!("{n}"); } fn main() { let count = 5; show_number(count); println!("still available: {count}"); }

Returning ownership from a function

bash
fn create_message() -> String { String::from("created inside function") } fn main() { let message = create_message(); println!("{message}"); }

Ownership of the returned String moves from the function into message.

Taking ownership and returning it

bash
fn add_suffix(mut text: String) -> String { text.push_str(" - updated"); text } fn main() { let name = String::from("Melochip"); let name = add_suffix(name); println!("{name}"); }

This works, but constantly moving values into functions and returning them becomes inconvenient. That is exactly why Rust provides references and borrowing, which we cover in Chapter 7.

The Drop concept

When an owning value leaves scope, Rust runs its cleanup logic. Types can implement the Drop trait for custom cleanup behavior.

bash
struct Connection { name: String, } impl Drop for Connection { fn drop(&mut self) { println!("Closing {}", self.name); } } fn main() { let _connection = Connection { name: String::from("demo-db"), }; println!("Using connection"); } // drop runs here

This deterministic cleanup model is useful for resources such as files, locks and network-related handles.

Move vs Copy vs Clone

OperationWhat happens?Original still usable?
MoveOwnership transfersNo
CopyCheap bitwise-style value copy for Copy typesYes
CloneExplicit duplication, potentially expensiveYes

Common ownership compiler errors

Use of moved value

bash
let first = String::from("hello"); let second = first; println!("{first}");

Fixes depend on intent:

  • Use the new owner ( second ).
  • Borrow the original instead of moving it.
  • Clone only if an independent copy is genuinely required.

Moving a field out of a value

Moving an owned field can partially move a struct. Later chapters on structs and borrowing show patterns that avoid unnecessary moves.

Practical example: process a job

bash
struct Job { id: u64, payload: String, } fn process(job: Job) { println!("Processing job {}: {}", job.id, job.payload); } fn main() { let job = Job { id: 101, payload: String::from("generate report"), }; process(job); // job is no longer available here because process owns it }

This design is sometimes desirable: once a job has been handed to the processor, the caller should not keep using the same owned job.

Hands-on mini lab

Create a project:

bash
cargo new ownership_lab cd ownership_lab

Try these exercises:

  1. Create a String , move it into another variable and observe the compiler error when reusing the old variable.
  2. Change the move to clone() and compare the result.
  3. Repeat the assignment with an i32 and see why both variables work.
  4. Create a function that takes ownership of a String .
  5. Create another function that returns a String .

Then run:

bash
cargo fmt cargo clippy cargo run

Key points to remember

  • Every value has an owner.
  • Ownership can move between variables and functions.
  • When an owner leaves scope, its value is dropped.
  • Copy types can be duplicated cheaply without invalidating the original.
  • clone() explicitly creates another value and may be expensive.
  • Borrowing lets code use a value without taking ownership.

Next chapter

Ownership becomes much more practical once you can temporarily access a value without moving it. Continue with Chapter 7: Rust Borrowing and References.

Official reference

For language-level reference and additional examples, see the official Rust Book section on Understanding Ownership.

Did this help?

Yes No

Join the discussion

0 Comments

Ask questions, add context, or share what worked.

Sign in to join the discussion

No comments yet. Start the discussion.

More from Melochip Editorial

Related articles

Stay in the loop. Get new Melochip articles

Receive Melochip article updates when new articles are available. Explicit opt-in only.

No pre-checked boxes. Unsubscribe support is part of the subscriber foundation.