본문으로 건너뛰기 Rust Ownership | Ownership, Borrowing, and Lifetimes

Rust Ownership | Ownership, Borrowing, and Lifetimes

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

  1. Ownership: one owner per value
  2. Move: transfer ownership
  3. Borrowing: use references (&, &mut)
  4. Lifetimes: how long references stay valid
  5. Safety: enforced at compile time

Next steps


Compared to other languages



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.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Rust, Ownership, Borrowing, Lifetimes, Memory safety.