Understanding Rust's Copy Trait with Stack and Heap Memory
Understanding Rust's Copy
Trait with Stack and Heap Memory
Rust is known for its powerful memory management system, which prevents common issues like memory leaks and data races. One of the key features that enable safe memory handling is the Copy
trait. In this article, we'll explore how the Copy
trait works in Rust and how it interacts with stack and heap memory.
Stack vs. Heap Memory in Rust
Rust uses two types of memory allocation: stack and heap. Understanding their differences is crucial for grasping Rust's ownership and Copy
behavior.
Stack Memory
✅ Fast, organized in Last-In-First-Out (LIFO) order ✅ Stores fixed-size values (e.g., integers, booleans, fixed-size arrays) ✅ Values stored in separate memory locations
Heap Memory
✅ Dynamically allocated memory for values that are not fixed in size ✅ Requires explicit deallocation (handled automatically in Rust via ownership) ✅ Stored separately, with a pointer on the stack referencing the data
The Copy
Trait and Stack Memory
The Copy
trait allows values to be duplicated via simple assignment (=
) without transferring ownership. Types stored entirely on the stack, like i32
, implement Copy
by default.
Example: Copying Stack Variables
fn main() {
let a: i32 = 5; // `a` is stored on the stack
let c = a; // `Copy` happens: a new independent value is created
println!("a: {}, c: {}", a, c); // Both `a` and `c` remain valid
}
Memory Layout (Stack Only)
Stack:
[a: 5] <- stored at address 0x001
[c: 5] <- stored at address 0x002 (independent copy)
📌 Since i32
implements Copy
, assigning a
to c
creates a new stack entry, meaning both a
and c
remain valid.
Heap Memory and Move Semantics
Unlike stack-allocated types, heap-allocated types do not implement Copy
by default because copying only their stack pointer would lead to double free errors or data corruption.
Example: Move Semantics with Heap Allocation
fn main() {
let s1 = String::from("Hello"); // `s1` stores a pointer on the stack, data on the heap
let s2 = s1; // Move happens (ownership transferred)
// println!("{}", s1); // ❌ ERROR: s1 is invalid after the move
println!("{}", s2); // ✅ s2 now owns the heap data
}
Memory Layout (Stack & Heap)
Stack:
[s1: ptr → 0x100] // `s1` holds a pointer to heap memory (ownership moved)
[s2: ptr → 0x100] // `s2` takes ownership
Heap:
[0x100: "Hello"] // Data remains unchanged, now owned by `s2`
📌 Since String
does not implement Copy
, ownership is moved to s2
, making s1
invalid.
How to Make Heap Types Copyable?
If you need a heap-allocated type to be copied instead of moved, use .clone()
to create a deep copy.
Example: Cloning a Heap Type
fn main() {
let s1 = String::from("Hello");
let s2 = s1.clone(); // Deep copy (new heap allocation)
println!("{}", s1); // ✅ `s1` is still valid
println!("{}", s2); // ✅ `s2` is a separate copy
}
Memory Layout (After Cloning)
Stack:
[s1: ptr → 0x100]
[s2: ptr → 0x200] // New heap allocation
Heap:
[0x100: "Hello"] // Original data
[0x200: "Hello"] // Cloned copy
📌 clone()
explicitly creates a deep copy of heap data, avoiding ownership transfer.
Comparison Table: Stack vs. Heap with Copy
Concept | i32 (Stack) | String (Heap) |
---|---|---|
Stored In | Stack | Heap (pointer on stack) |
Implements Copy ? | ✅ Yes | ❌ No |
Assignment (= ) | Copies value | Moves ownership |
After Assignment | Both variables remain valid | Original variable becomes invalid |
Use .clone() ? | ❌ Not needed | ✅ Required for deep copy |
Key Takeaways
✔ Copy
is only implemented for types that are entirely stored on the stack (e.g., i32
, bool
, char
).
✔ Heap-allocated types (e.g., String
, Vec<T>
) use move semantics to prevent double frees.
✔ Use .clone()
when you want a heap-allocated value to be copied instead of moved.
✔ Understanding Rust’s memory model helps avoid common pitfalls like use-after-move errors.
Comments
Post a Comment