Welcome to your first lesson. This course is designed to build your practical Rust skills, moving from core concepts to building the command-line applications you're interested in. We'll start this journey by tackling Rust's most distinctive feature: the ownership system. While you've read about it, our goal here is to make it feel intuitive through hands-on practice.
In this lesson, you will learn to write functions that transfer ownership of data. We will focus on data that lives on the "heap," and you'll see precisely why the original variable can no longer be used after its value has been moved. This concept is a fundamental departure from memory management in garbage-collected languages like Python, and mastering it is the first major step toward writing safe and efficient Rust code.
The Two Homes for Your Data: Stack and Heap
To understand ownership, we first need to understand where your program stores data. In systems programming, memory is typically managed in two main areas: the stack and the heap. Your background in electronics engineering gives you a strong foundation in how computer architecture works, so think of the stack as a highly organized, fast-access memory region, much like a CPU's register file, while the heap is more like general-purpose RAM that is allocated on demand.
-
The Stack: This is where data with a known, fixed size is stored. It's incredibly fast because it operates in a "last-in, first-out" (LIFO) manner. When a function is called, its local variables are pushed onto the stack. When the function finishes, they are popped off. This includes types like integers (
i32), booleans (bool), and floating-point numbers (f64). -
The Heap: This is for data whose size might change or is unknown at compile time. Think of a
Stringthat can grow as you add text, or aVec<T>(a growable list or vector) that can hold a variable number of elements. Allocating memory on the heap is slower because the system has to find a suitable empty spot. Accessing this data also involves an extra step, as the stack will hold a pointer to the data's location on the heap.
The following video provides an excellent visual explanation of this distinction and sets the stage for why ownership is necessary to manage heap data safely.
Do you know who OWNS your DATA? | Rust Fundamentals Part 5
Watch this segment from the "Do you know who OWNS your DATA?" video by Green Tea Coding to see how the stack and heap work.
Please watch from the beginning until the timestamp 7:28. The video covers: The general concept of memory management. A clear, animated explanation of how the stack operates with fixed-size data. How the heap is used for dynamic data and the memory management challenges it introduces, which Rust's ownership system is designed to solve.
The key problem, as the video highlights, is managing the lifetime of data on the heap. If multiple parts of a program point to the same heap data, which one is responsible for cleaning it up? And how do we prevent using data after it has been cleaned up (a "use-after-free" bug)? Rust's answer is ownership.
The Rules of Ownership and the "Move" Semantic
Rust enforces memory safety at compile time through a set of simple but strict rules:
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (i.e., the memory is freed).
This "one owner" rule is what prevents the chaos of multiple parts of a program trying to manage the same memory. When you assign a heap-allocated value from one variable to another, Rust doesn't copy the heap data (which would be slow). Instead, it performs a move.
A move transfers ownership of the heap data to the new variable. To enforce the "one owner" rule, the compiler then makes the original variable invalid.
Let's look at this in action. Here's a typical scenario:
fn main() {
// s1 owns a String allocated on the heap.
let s1 = String::from("hello");
// Ownership of the String is "moved" from s1 to s2.
// s1 is no longer a valid owner.
let s2 = s1;
// This would fail to compile because s1 is no longer valid.
// println!("s1 = {}", s1);
// This is fine, because s2 is the current owner.
println!("s2 = {}", s2);
}
The sequence diagram below visualizes how ownership is transferred when a Vec (another heap-allocated type) is moved from one binding to another.

Not all data types are moved, however. Simple types that live entirely on the stack, like i32, implement the Copy trait. For these types, an assignment creates a true copy, and both variables remain valid.
The following article explains the mechanics of moves and the Copy trait in more detail.
Rust Move and Reference Semantics: A Complete Guide
This guide clearly explains when moves happen and why some types are copied instead.
Please read the following sections: Start at the heading Move Semantics and read down to the end of the code block under "Returning from Functions". This covers the three situations where moves occur. Continue with the section Copy vs Move, which contrasts types that are moved with those that are copied.
Transferring Ownership with Functions
The principles of moving ownership apply directly to function calls. This is one of the most common ways you'll see ownership transferred.
- Passing a value to a function moves ownership to the function's parameter. The original variable in the calling scope becomes invalid, just as with an assignment.
- Returning a value from a function moves ownership out of the function to the variable that receives the return value.
Consider this example, which fails to compile for the exact reason we've been discussing.
fn main() {
let my_string = String::from("Rust is powerful");
// Ownership of the String is moved into the function.
process_data(my_string);
// Error! We can't use my_string here because it was moved.
// println!("{}", my_string);
}
fn process_data(data: String) {
// The 'data' parameter now owns the String.
println!("Processing: {}", data);
// When 'process_data' ends, 'data' goes out of scope and the String's
// memory is dropped (freed).
}
The Rust compiler is famously helpful in these situations. If you were to compile the code above (with the final println! uncommented), you would get a specific error, E0382: borrow of moved value.

To solidify this, watch this next segment that explicitly demonstrates ownership transfer with functions.
Understanding Ownership in Rust
This clip from the "Understanding Ownership in Rust" video by Let's Get Rusty walks through passing ownership into functions and getting it back via return values.
Please watch the section from 10:28 to 12:10. It covers: How passing an argument to a function can move ownership. How returning a value from a function also transfers ownership.
Your Turn: Take and Return Ownership
Now, let's put this into practice. Your task is to complete a small program by implementing a function that takes ownership of a vector, modifies it, and then returns ownership of the modified vector to the caller.
Instructions:
- Set up a new project using
cargo new ownership_practiceand navigate into the directory. - Open
src/main.rsand replace its content with the code below. - Implement the
add_elementfunction according to the// TODOcomment. - Run
cargo runto see the output. - Uncomment the line
// println!("Original vector: {:?}", my_vector);and runcargo check. Observe the compiler error and make sure you understand why it happens.
fn main() {
// This vector is created on the heap, and 'my_vector' is its owner.
let my_vector = vec![10, 20, 30];
println!("Original vector created. About to transfer ownership...");
// This call will move ownership of the vector to the `add_element` function.
// `add_element` will then return ownership of the modified vector,
// which we assign to a new variable, `new_vector`.
let new_vector = add_element(my_vector);
// This line should cause a compile error because the value of `my_vector` has been moved.
// Try uncommenting it to see the error for yourself!
// println!("Original vector: {:?}", my_vector);
println!("Received ownership back. New vector is: {:?}", new_vector);
}
// TODO: Implement the `add_element` function.
// It should accept one argument: a Vec<i32>, taking ownership.
// The function should be declared as mutable, add the number 40 to the vector,
// and then return the vector, transferring ownership back to the caller.
fn add_element(mut v: Vec<i32>) -> Vec<i32> {
v.push(40);
v // This returns ownership of the modified vector.
}
This simple exercise simulates a common pattern in Rust: transforming data by passing it through a chain of functions, with each function taking ownership, modifying the data, and passing ownership to the next.
Conclusion
In this lesson, we established the "why" and "how" of Rust's ownership system for heap-allocated data.
Key Takeaways:
- Rust uses the stack for fixed-size data and the heap for dynamic or growable data.
- To ensure memory safety without a garbage collector, Rust enforces that every value has exactly one owner.
- When you assign a heap-allocated value or pass it to a function, you move it, transferring ownership.
- The Rust compiler invalidates the original variable after a move to prevent errors like trying to free the same memory twice.
- Functions can return ownership of a value to their caller.
This pattern of moving ownership into and out of functions is fundamental, but it can be verbose. What if you just want a function to read a value without needing to own it? Constantly passing ownership back and forth would be inefficient.
In our next lesson, we will explore the solution: borrowing. You will learn how to use references to grant temporary, non-owning access to data, allowing you to write much more flexible and efficient functions.