Skip to content
DocsRust LearningintermediateOwnership
Chapter 4 of 19·intermediate·8 min read

Ownership

Quyền Sở Hữu

Rust's unique memory management model

Hover or tap any paragraph to see Vietnamese translation

What Is Ownership?

Ownership is Rust's most unique and defining feature, with deep implications throughout the entire language. It enables Rust to guarantee memory safety without a garbage collector — something most other safe languages cannot do.

All programs must manage memory. Languages like JavaScript, Python, and Go use a garbage collector that automatically frees memory. Others like C and C++ require manual management. Rust takes a third approach: memory is managed through the ownership system, with rules checked at compile time — there is no runtime cost.

Stack and Heap Memory

Understanding the difference between stack and heap is essential for understanding ownership. Both are parts of memory available at runtime, but they have very different structures and characteristics.

  • Stack stores data in LIFO (Last In, First Out) order. Data size must be known at compile time. Very fast because only the stack pointer needs to move up or down.
  • Heap allocates data at runtime and can vary in size. Slower than stack because it requires finding a large enough free memory region and tracking allocations via pointers.
  • Ownership primarily addresses heap data: tracking what code is using heap data, minimizing duplicate data, and cleaning up unused data.
src/main.rs
1// Stack data: size known at compile time2let x: i32 = 5;      // stored entirely on the stack3let y: bool = true;  // stored entirely on the stack4let arr: [i32; 3] = [1, 2, 3]; // stored on the stack56// Heap data: size not known at compile time, or can grow7let s = String::from("hello"); // metadata on stack, content on heap8let v = vec![1, 2, 3];        // metadata on stack, elements on heap

The Three Ownership Rules

The entire Rust ownership system is built on three fundamental rules. Violating any rule produces a compile error. These are the foundation of everything memory-related in Rust.

  • Each value in Rust has a variable called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value is dropped and memory is automatically freed.
Info
These three rules seem simple but have wide-ranging implications. They are why Rust can guarantee no double-free errors, use-after-free bugs, or memory leaks without a garbage collector.

Variable Scope

Scope is the range within a program for which an item is valid. In Rust, when a variable goes out of scope (the closing curly brace), Rust automatically calls the drop function to free that variable's memory.

src/main.rs
1fn main() {2    // s is not valid here — not yet declared34    {5        let s = String::from("hello"); // s is valid from this point67        println!("{s}");    // do stuff with s8    }9    // The scope is now over. Rust calls drop(s) automatically.10    // s is no longer valid — the heap memory is freed.1112    // println!("{s}"); // ERROR: use of possibly-uninitialized `s`1314    // This same pattern applies to any type that owns heap resources:15    {16        let v = vec![1, 2, 3];17        println!("{v:?}");18    } // v is dropped here — Vec frees its heap memory19}

The String Type and Heap Allocation

To understand ownership more deeply, we need a more complex data type. String is the perfect example because its size is not known at compile time and it is stored on the heap. This differs from string literals (&str), which are immutable and embedded directly in the binary.

src/main.rs
1fn main() {2    // String literal: immutable, hardcoded in the binary, type is &str3    let s1: &str = "hello";45    // String: mutable, heap-allocated, growable, type is String6    let mut s2 = String::from("hello");7    s2.push_str(", world!"); // appends to the String8    s2.push('!');            // appends a single character910    println!("{s1}");  // hello11    println!("{s2}");  // hello, world!!1213    // String has three parts stored on the stack:14    // 1. ptr — pointer to the heap memory holding the content15    // 2. len — current length in bytes16    // 3. capacity — total bytes allocated on the heap17    println!("len={}, capacity={}", s2.len(), s2.capacity());18} // s2 goes out of scope; drop is called; heap memory is freed19  // s1 has no heap memory to free

Move Semantics

When you assign a heap-allocated variable to another, Rust moves ownership — the original variable becomes invalid. This prevents double-free errors: only the new owner drops the data when it goes out of scope.

src/main.rs
1fn main() {2    let s1 = String::from("hello");3    let s2 = s1; // s1 is MOVED to s2 — s1 is now invalid45    // println!("{s1}"); // ERROR: borrow of moved value: `s1`6    println!("{s2}"); // OK — s2 is the new owner78    // Why not a "shallow copy"?9    // Rust invalidates s1 to prevent BOTH s1 and s2 from trying10    // to free the same heap memory when they go out of scope.1112    // Stack-only types (Copy types) behave differently:
Info
"Move" in Rust does not mean heap data is physically moved in memory. Only the stack metadata (pointer, length, capacity) is copied, and the old variable is invalidated. There is zero runtime cost.

Clone and the Copy Trait

Deep copying with clone

If you want to deeply copy the heap data of a String, use the clone method. This is a computationally expensive operation — when you see clone in code, you know that some potentially costly code is being executed.

src/main.rs
1fn main() {2    let s1 = String::from("hello");3    let s2 = s1.clone(); // deep copy — duplicates heap data45    println!("s1={s1}, s2={s2}"); // Both valid — s1 was not moved6}

The Copy trait for stack types

Types stored entirely on the stack implement the Copy trait and are automatically copied on assignment — no move occurs. A type can implement Copy if and only if all of its components also implement Copy.

src/main.rs
1fn main() {2    // These types all implement Copy:3    let a: i32  = 5;    let b = a;  // copied4    let c: f64  = 3.14; let d = c;  // copied5    let e: bool = true; let f = e;  // copied6    let g: char = 'z';  let h = g;  // copied78    // Tuples implement Copy only if all fields implement Copy:9    let t1: (i32, f64) = (1, 2.0);10    let t2 = t1; // copied — both i32 and f64 are Copy1112    // Fixed-size arrays of Copy types also implement Copy:

Ownership and Functions

Passing a value to a function follows the same semantics as assigning to a variable: passing a String moves it, passing an i32 copies it. After the function takes ownership, the original variable is no longer valid.

src/main.rs
1fn main() {2    let s = String::from("hello");34    takes_ownership(s);       // s's value moves into the function5    // println!("{s}");       // ERROR: s was moved — no longer valid here67    let x = 5;8    makes_copy(x);            // i32 is Copy — x is still valid9    println!("{x}");          // OK10}1112fn takes_ownership(some_string: String) {13    println!("{some_string}");14} // some_string goes out of scope; drop is called; heap memory freed1516fn makes_copy(some_integer: i32) {17    println!("{some_integer}");18} // some_integer goes out of scope — nothing special happens (no heap)

Return Values and Scope

Returning values from a function also transfers ownership. The pattern of taking ownership and returning it is cumbersome. This is exactly why Rust has references: they let a function use a value without taking ownership — the topic of the next chapter.

src/main.rs
1fn main() {2    // gives_ownership transfers its return value to s13    let s1 = gives_ownership();4    println!("{s1}");56    let s2 = String::from("hello");78    // s2 is moved into takes_and_gives_back9    // which also returns a value, moved into s310    let s3 = takes_and_gives_back(s2);11    // println!("{s2}"); // ERROR: s2 was moved12    println!("{s3}");
Tip
The take-and-return-ownership pattern in calculate_length_verbose is very cumbersome. In practice you use references (&String) so a function can read data without taking ownership. References are the topic of the References and Borrowing chapter.

Key Takeaways

Điểm Chính

  • Each value in Rust has exactly one ownerMỗi giá trị trong Rust có đúng một chủ sở hữu
  • When the owner goes out of scope, the value is droppedKhi chủ sở hữu ra khỏi phạm vi, giá trị sẽ được giải phóng
  • Assignment moves ownership by default for heap dataGán giá trị di chuyển quyền sở hữu mặc định cho dữ liệu trên heap
  • Types implementing Copy are duplicated instead of movedCác kiểu triển khai Copy được sao chép thay vì di chuyển

Practice

Test your understanding of this chapter

Quiz

Which of the following is NOT one of Rust's three ownership rules?

Điều nào sau đây KHÔNG phải là một trong ba quy tắc quyền sở hữu của Rust?

Quiz

What happens when you assign a String to another variable (let s2 = s1)?

Điều gì xảy ra khi bạn gán một String cho biến khác (let s2 = s1)?

True or False

Stack-only types like i32 and bool implement the Copy trait, so assigning them to another variable does not move ownership.

Các kiểu chỉ lưu trên stack như i32 và bool triển khai trait Copy, nên khi gán cho biến khác không chuyển quyền sở hữu.

Code Challenge

Fix the double-use error

Sửa lỗi sử dụng biến đã chuyển quyền sở hữu

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.;
    println!("{s1}, {s2}");
}
True or False

Rust automatically calls the drop function and frees heap memory when a variable goes out of scope — no garbage collector or manual free is needed.

Rust tự động gọi hàm drop và giải phóng bộ nhớ heap khi biến ra khỏi phạm vi — không cần garbage collector hay giải phóng thủ công.

Chapter Complete!

Great job! Keep the momentum going.

Your progress0 of 19 chapters read
← → to navigate chapters
Built: 4/8/2026, 12:01:11 PM