JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Ownership and Move Semantics

In C you decide who frees each block and you keep that decision in your head; get it wrong and you leak or double-free. Rust turns that decision into a rule the compiler tracks: every value has exactly one owner, and assigning or passing it moves ownership rather than quietly making a second owner. Once you see how a move works, free() without free() stops feeling like magic.

The question C never answers: who frees this?

The previous guide made the promise: Rust prevents use-after-free, double-free, and leaks at compile time, with no garbage collector. This guide cashes that promise by explaining the one idea everything else is built on. Start from a question you have answered a hundred times in C, always informally. When you call malloc() and get back a block, who is responsible for calling free() on it? The C language does not record an answer. The block is just an address; the duty to free it lives in your head, in a comment, maybe in a function's documentation — never in the types. If two parts of the program each think they own the block, they both free it and you have a double-free; if neither thinks so, nobody frees it and you have a memory leak.

Good C programmers handle this with discipline. They keep firm conventions about who owns an allocation: a function name like `make_widget` says "I return a block you now own and must free"; a function name like `widget_size` says "I only borrow your pointer, I will not free it." That discipline is real, and it works — right up until a refactor moves a free() to the wrong side of a boundary, or an early `return` skips the cleanup, or two error paths both decide to release the same buffer. The convention was never checked by anything. It was a promise between humans, and humans forget.

Rust's move is to take exactly this informal notion — "who is responsible for freeing this" — and promote it from a convention into a rule the compiler tracks on every line. That rule is called ownership, and it is deliberately narrow: it does not say who may read a value (many places can, as the next guide shows), only who is responsible for cleaning it up. Keep that distinction sharp from the start, because it is the one beginners most often blur.

The ownership rule in three lines

The whole rule fits in three sentences. First, each value has exactly one owning variable — the variable a value is bound to. Second, there can be only one owner at any moment; you can never have two variables that both own the same value. Third, when the owner goes out of scope — when the block it was declared in ends — Rust automatically cleans the value up. That cleanup is called drop, and for a value backed by the heap like a `String` it does precisely what you would have written by hand in C: it frees the heap memory. You never type the word for free; the end of the owner's scope is the free.

Look at how much that buys you. Because exactly one variable owns the value, there is exactly one place and one moment the memory is released — so a double-free is structurally impossible: only the owner frees, and only once, at scope's end. And because the compiler inserts that free for you whenever an owner falls out of scope, you cannot leak by simply forgetting; forgetting is no longer a thing you can do. This is the same outcome the disciplined C programmer was chasing with naming conventions and careful `goto cleanup` ladders, but the compiler now guarantees it instead of trusting you to. The same machinery is the subject of deterministic drop, which runs cleanup at a known point in the code rather than "sometime later" the way a garbage collector would.

Make it concrete. Write `let s = String::from("hello");` inside a function and s now owns a heap-backed string; you can read through it freely, say with `s.len()`. When the function's block ends at its closing brace, s goes out of scope, the compiler runs `drop(s)`, and the heap memory is freed — at that exact brace, every time, with no `free()` typed anywhere. The closing brace of the owner's scope is the free. That is the whole shape of cleanup in Rust, and the next idea — what happens when you move s somewhere else before that brace — is where it gets interesting.

Assignment is a move, not a copy

The one-owner rule sounds clean until you ask the obvious question: programs hand values around constantly. You assign `let b = a`, you pass arguments, you return results. If every value must have exactly one owner, what happens to ownership the moment you write `let b = a`? In C, copying a pointer just produces a second pointer to the same block — and now two variables both believe they are in charge, which is precisely the situation that breeds double-frees. Rust cannot allow that without breaking its own rule. Its answer is the move.

Here is the mental picture that makes it click. A `String` is two things in two places: a small fixed-size record sitting on the stack — holding a pointer to the characters, a length, and a capacity — and the actual characters living out on the heap. When you write `let b = a`, Rust copies the small stack record into b, but it does not touch the heap characters, and crucially it marks the variable a as no longer valid. Now b holds the only pointer to the heap data — b is the sole owner — and a is dead. Touch a again and the compiler stops you cold with `error[E0382]: borrow of moved value: 'a'`. Ownership moved from a to b. There is still exactly one owner, so the heap memory will still be freed exactly once, when b's scope ends.

the stack record copied; the heap NOT copied; source invalidated

  before  let b = a;            after  let b = a;
  -------------------            ----------------------
  a: [ ptr | len | cap ]         a: (invalidated -- use is an error)
        |                        b: [ ptr | len | cap ]
        v                              |
  heap: "hi"                           v
                                 heap: "hi"   (same bytes, one owner)

// MOVE = copy the small stack record + mark the source dead.
// Result: still exactly one owner, so still exactly one free.
A move duplicates only the tiny stack record and invalidates the source; the heap bytes are never copied and never double-freed.

Passing to a function moves too — and how to give it back

Passing a value to a function is the same event as `let b = a`, just with the destination being the function's parameter. If you call `greet(s)` where greet takes a `String` by value, ownership moves out of s and into greet's parameter. When greet returns, that parameter goes out of scope, drop runs, and the string is freed — so after the call, s in the caller is invalid. This surprises nearly everyone the first time: you handed your `String` to a function expecting to use it again afterwards, and the compiler tells you it has been moved away. It is the honest consequence of the rule, not a quirk: by-value really does mean "you give it up."

You have three honest ways out, and choosing among them is most of day-to-day Rust. One: if the function really should consume the value (take it and dispose of it), let it move in, and that is correct. Two: if the function should hand the value back, have it return the `String`, so ownership moves out and then moves back — clumsy, but it works and it is explicit. Three, and overwhelmingly the common choice: lend the value instead of giving it, by passing a reference with `&`. That is borrowing, the subject of the very next guide, and it exists precisely so that reading a value does not have to consume it.

Put numbers on it. Suppose `greet` is declared `fn greet(name: String)` — it takes a `String` by value — and you call `greet(s)` after `let s = String::from("ada");`. The call moves ownership into greet's parameter name; when greet's body finishes, name is dropped and the string freed; and back in the caller, a later `println!("{s}")` fails to compile with `error[E0382]: borrow of moved value: 's'`. Change the signature to `fn greet(name: &String)` and call `greet(&s)` instead, and you have lent the value rather than surrendered it — s stays yours, usable after the call. That single change from `String` to `&String` is the doorway into the next guide.

Copy types: the small, honest exception

If `let b = a` always moved and invalidated a, then `let y = x` on a plain integer would make x unusable — which would be absurd and miserable. So there is one carefully bounded exception. Small, plain values that live entirely on the stack and own no heap resource — integers like `i32`, floating-point numbers, `bool`, `char`, and tuples built only from such things — implement a marker called Copy. For a Copy type, `let b = a` duplicates the bits and leaves a perfectly usable, because copying a few stack bytes is cheap and there is no single heap resource for two owners to fight over. No invalidation, no move; you just have two independent values.

So the rule of thumb is: if a value owns a resource (a `String`, a `Vec`, a file handle), assigning or passing it moves it and invalidates the source; if it is a simple Copy value (an `i32`), it is copied and the source lives on. The honest subtlety — and this trips up newcomers from every other language — is that move is the default and Copy is the special case. Most languages quietly copy or quietly share on every assignment; Rust does neither by default. It transfers responsibility, and only the cheap, resource-free types opt out into copying.

Side by side, the two behaviours are stark. With `let a = String::from("hi"); let b = a;` the assignment moves: b owns it now, a is invalidated, and a later `println!("{a}")` is the familiar `error[E0382]: borrow of moved value: 'a'`. With `let x = 5; let y = x;` the assignment copies, because `i32` is Copy: the bits are duplicated, x is still usable, and `println!("{x} {y}")` happily prints `5 5`. Same syntax, `let _ = _`, two opposite outcomes — and the difference is entirely whether the type owns a heap resource.

Why this is the foundation, honestly

Step back and see the shape of what you now hold. Ownership names who is responsible for a value; move keeps that responsibility singular as the value travels from variable to variable and into functions; drop spends that responsibility — exactly once, at a known point — by freeing the memory. Together they make double-free structurally impossible (one owner, one free), make leaks-by-forgetting impossible (scope's end always frees), and set up the only remaining problem: how do you read a value without consuming it? That problem is what borrowing and the borrow checker solve, and they are the next two guides. Ownership is the soil everything else grows in.

Now the honest caveats, because this guide would be dishonest without them. Ownership is not free to the programmer — it is the feature people fight hardest at first, and the friction is real. Code that would compile fine in C (and run fine, on a good day) will be rejected here, and the fix is usually to restructure who owns what rather than to argue with the compiler. Ownership also does not prevent every bug: it stops use-after-free, double-free, and forget-to-free, but it does nothing about logic errors, and you can still leak on purpose (a reference cycle, or an explicit leak) — Rust prevents accidental leaks, not all leaks. And the guarantee covers safe Rust only; inside an `unsafe` block you are back to C-style responsibility. As the first guide insisted, this is a different set of tradeoffs, not the end of all bugs.