What Is Ownership?
Ownership is Rust’s memory management system with compile-time rules that prevent memory safety issues without runtime overhead. Unlike garbage-collected languages, Rust determines memory cleanup at compile time.
Stack vs Heap
Stack: LIFO structure for fixed-size data. Fast allocation/deallocation, automatic cleanup. Heap: Runtime allocation for dynamic-size data. Returns pointer, requires explicit management.
Stack access is faster due to locality. Heap access requires pointer dereferencing. Ownership primarily manages heap data.
Ownership Rules
- Each value has exactly one owner
- Only one owner at a time
- Value is dropped when owner goes out of scope
Variable Scope
let s = "hello";
String literals are immutable and stack-allocated. The variable s
is valid from declaration until scope end.
fn main() { { // s is not valid here, since it's not yet declared let s = "hello"; // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no longer valid }
The String
Type
String
manages heap-allocated, mutable text data:
let s = String::from("hello");
Unlike string literals, String
can be mutated and has dynamic size:
fn main() { let mut s = String::from("hello"); s.push_str(", world!"); // push_str() appends a literal to a String println!("{s}"); // this will print `hello, world!` }
Memory and Allocation
String
requires heap allocation for dynamic content. Rust automatically calls drop
when the owner goes out of scope—similar to RAII in C++.
fn main() { { let s = String::from("hello"); // s is valid from this point forward // do stuff with s } // this scope is now over, and s is no // longer valid }
Variables and Data Interacting with Move
Simple stack values are copied:
fn main() { let x = 5; let y = x; }
Heap-allocated values are moved:
fn main() { let s1 = String::from("hello"); let s2 = s1; }
String
contains three parts on the stack: pointer, length, capacity. Assignment copies these metadata but not heap data. Rust invalidates the original variable to prevent double-free errors.
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
}
Error: value used after move. This prevents the double-free memory safety issue.
Scope and Assignment
Assigning a new value immediately drops the previous value:
fn main() { let mut s = String::from("hello"); s = String::from("ahoy"); println!("{s}, world!"); }
Variables and Data Interacting with Clone
For explicit deep copying:
fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {s1}, s2 = {s2}"); }
clone()
is expensive—it copies heap data.
Stack-Only Data: Copy
Types implementing Copy
trait are duplicated instead of moved:
fn main() { let x = 5; let y = x; println!("x = {x}, y = {y}"); }
Copy
types include:
- Integer types (
u32
,i32
, etc.) - Boolean (
bool
) - Floating-point types (
f64
) - Character (
char
) - Tuples of
Copy
types
Types with Drop
trait cannot implement Copy
.
Ownership and Functions
Function calls behave like assignment—values are moved or copied:
fn main() { let s = String::from("hello"); // s comes into scope takes_ownership(s); // s's value moves into the function... // ... and so is no longer valid here let x = 5; // x comes into scope makes_copy(x); // Because i32 implements the Copy trait, // x does NOT move into the function, // so it's okay to use x afterward. } // Here, x goes out of scope, then s. However, because s's value was moved, // nothing special happens. fn takes_ownership(some_string: String) { // some_string comes into scope println!("{some_string}"); } // Here, some_string goes out of scope and `drop` is called. The backing // memory is freed. fn makes_copy(some_integer: i32) { // some_integer comes into scope println!("{some_integer}"); } // Here, some_integer goes out of scope. Nothing special happens.
Return Values and Scope
Functions can transfer ownership via return values:
fn main() { let s1 = gives_ownership(); // gives_ownership moves its return // value into s1 let s2 = String::from("hello"); // s2 comes into scope let s3 = takes_and_gives_back(s2); // s2 is moved into // takes_and_gives_back, which also // moves its return value into s3 } // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing // happens. s1 goes out of scope and is dropped. fn gives_ownership() -> String { // gives_ownership will move its // return value into the function // that calls it let some_string = String::from("yours"); // some_string comes into scope some_string // some_string is returned and // moves out to the calling // function } // This function takes a String and returns a String. fn takes_and_gives_back(a_string: String) -> String { // a_string comes into // scope a_string // a_string is returned and moves out to the calling function }
Returning ownership for every function parameter is verbose. Rust provides references for this use case:
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); println!("The length of '{s2}' is {len}."); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len() returns the length of a String (s, length) }