Rust Ownership | Ownership, Borrowing, and Lifetimes
이 글의 핵심
Deep dive into Rust ownership: move and copy, functions and ownership, references, slices, lifetime annotations, and patterns compared to C++—with runnable examples.
Introduction
Ownership is Rust’s signature feature: memory safety without a garbage collector by enforcing rules at compile time. Think of each heap value as a single key to an apartment—only one variable holds the key; when it goes out of scope, Rust locks the door (drops) exactly once. There is no duplicate key that could double-free. Compare with C++ [smart pointers](/en/blog/cpp-comparison-04-shared-unique-ptr/ and Python’s GC-backed objects.
1. Ownership rules
Rule 1: Every value has an owner
fn main() {
let s = String::from("hello");
// `s` owns the heap-allocated string
}
What this means: After String::from allocates on the heap, s is the sole owner of that memory. When s leaves scope, Rust calls drop—no manual free like in C.
Rule 2: Only one owner
Each value has exactly one owner at a time:
fn main() {
let s1 = String::from("hello");
// Move: ownership transfers fully to s2
// s1 is no longer valid
let s2 = s1;
// println!("{}", s1); // Compile error!
// "value borrowed here after move"
println!("{}", s2); // OK — s2 owns the data
}
// When s2 goes out of scope, the string is dropped once
Why move? C++ pitfall:
// Shallow copy can lead to double free if not careful
std::string s1 = "hello";
std::string s2 = s1; // Both may share resources depending on implementation
// Destructors can run twice on the same resource if misdesigned
Perspective: In C++, if you do not consistently use value semantics, it is easy to get copy/move/destructor rules wrong. Rust defaults to move and makes expensive copies explicit with clone().
Rust’s approach:
- Deep copies are costly (allocation + memcpy)
- By default, ownership moves (transfer + invalidate source)
- Double free is impossible (single owner)
- Need a duplicate? Call
clone()explicitly
let s1 = String::from("hello");
let s2 = s1.clone(); // Deep copy (explicit)
println!("{}, {}", s1, s2); // Both OK
clone in production: It can be CPU- and memory-heavy—profile hot loops and avoid redundant clones. Sometimes you choose clone() anyway to simplify APIs when correctness matters more than micro-optimization.
The Copy trait:
// Small stack types (integers, bool, etc.) implement Copy
// Assignment copies instead of moving
let x = 5;
let y = x; // Copy, not move
println!("{}, {}", x, y); // Both OK
// Cheap to copy: fixed small size on the stack
Rule 3: Drop at end of scope
fn main() {
{
let s = String::from("hello");
println!("{}", s);
} // `drop` runs for `s` here
// println!("{}", s); // Error: s is gone
}
RAII: Use the same pattern for resources (files, locks, connections) scoped to a block—cleanup runs automatically. This ties into the Drop trait and idiomatic resource management in Rust.
2. Functions and ownership
Moving into a function
fn main() {
let s = String::from("hello");
takes_ownership(s);
// println!("{}", s); // Error: ownership moved
}
fn takes_ownership(s: String) {
println!("{}", s);
} // `s` is dropped here
Passing by value: Giving a String to a function consumes it for the caller. Use .clone() or references (&String / &str) if the caller must keep using it. Many codebases distinguish consuming APIs from borrowing APIs by naming and signatures.
Returning ownership
fn main() {
let s1 = gives_ownership();
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);
println!("{}, {}", s1, s3);
}
fn gives_ownership() -> String {
String::from("hello")
}
fn takes_and_gives_back(s: String) -> String {
s
}
3. References and borrowing
Immutable references (&)
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("{} length: {}", s1, len); // s1 still usable
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s is a reference; nothing is dropped here
Mutable references (&mut)
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // hello, world
}
fn change(s: &mut String) {
s.push_str(", world");
}
The borrowing rules
Rust’s rules prevent data races at compile time:
fn main() {
let mut s = String::from("hello");
// Multiple immutable borrows are allowed
let r1 = &s;
let r2 = &s;
println!("{}, {}", r1, r2); // OK
// Only one mutable borrow at a time
let r3 = &mut s;
// let r4 = &mut s; // Error!
println!("{}", r3); // OK
// Cannot mix & and &mut in the same live region
let r5 = &s;
// let r6 = &mut s; // Error!
println!("{}", r5);
}
4. Slices
A slice references a contiguous portion of a collection:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5]; // "hello" as &str
let world = &s[6..11]; // "world"
println!("{}, {}", hello, world);
let hello2 = &s[..5];
let world2 = &s[6..];
let full = &s[..];
}
// First word: slice example
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let sentence = String::from("hello world");
let word = first_word(&sentence);
println!("first word: {}", word);
// If `word` borrows `sentence`, mutating `sentence` can fail to compile:
// let mut s = String::from("hello world");
// let word = first_word(&s);
// s.clear(); // Error while `word` is live
}
Slice types:
let s: String = String::from("hello");
let slice: &str = &s[0..2]; // "he"
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..3]; // [2, 3]
// Slice = pointer + length; out-of-range access panics
5. Lifetimes
Lifetime annotations
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let s1 = String::from("long string");
let s2 = String::from("short");
let result = longest(&s1, &s2);
println!("longest: {}", result);
}
Struct lifetimes
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("{}", excerpt.part);
}
6. Hands-on example
String processing
fn main() {
let text = String::from("hello rust world");
let words = split_words(&text);
println!("words: {:?}", words);
let first = first_word(&text);
println!("first word: {}", first);
}
fn split_words(s: &String) -> Vec<&str> {
s.split_whitespace().collect()
}
fn first_word(s: &String) -> &str {
s.split_whitespace().next().unwrap_or("")
}
Summary
Takeaways
- Ownership: one owner per value
- Move: transfer ownership
- Borrowing: use references (
&,&mut) - Lifetimes: how long references stay valid
- Safety: enforced at compile time
Next steps
Compared to other languages
Related posts
- C++ vs Rust: ownership, safety, errors, concurrency, performance
- Rust memory safety deep dive | Borrow checker and lifetimes
- Rust vs C++ memory safety | Compiler errors [#47-3]
- C++ observer pointer
- C++ and Rust interoperability [#44-2]
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Deep dive into Rust ownership: move and copy, functions and ownership, references, slices, lifetime annotations, and.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Getting Started with Rust | Memory-Safe Systems Programming
- Rust Memory Safety: Ownership, Borrowing, Lifetimes, unsafe
- C++ shared_ptr vs unique_ptr: Smart Pointer Choice Complete
- Python Data Types | Lists· Dictionaries
Keywords Covered in This Article (Related Search Terms)
This article covers Rust, Ownership, Borrowing, Lifetimes, Memory safety.