6.1 Overview of Ownership
Ownership is the cornerstone of Rust's memory management system. It enables Rust to guarantee memory safety at compile time, preventing many common errors that can occur in C. Understanding ownership is crucial for mastering Rust.
6.1.1 Ownership Rules
Rust enforces a set of rules for ownership:
- Each value in Rust has a single owner.
- When the owner goes out of scope, the value is dropped (memory is freed).
- Ownership can be transferred (moved) to another variable.
- There can only be one owner at a time.
These rules are enforced at compile time by the borrow checker, ensuring memory safety without runtime overhead. The borrow checker analyzes your code to enforce these ownership and borrowing rules, preventing data races, dangling pointers, and other memory safety issues.
Types in Rust can implement the Drop
trait to customize what happens when they go out of scope. This allows you to define custom cleanup logic, similar to destructors in C++.
Example: Scope and Drop
fn main() {
{
let s = String::from("hello"); // s comes into scope
// use s
} // s goes out of scope and is dropped here
}
In this example, s
is a String
that is created within an inner scope. When the scope ends, s
is automatically dropped, and its memory is freed. This automatic cleanup is similar to C++'s RAII (Resource Acquisition Is Initialization) pattern but is enforced by the compiler in Rust.
Comparison with C
In C, memory management is manual:
#include <stdio.h>
#include <stdlib.h>
#include <string.h> // for strcpy
int main() {
{
char *s = malloc(6); // Allocate memory on the heap
strcpy(s, "hello");
// use s
free(s); // Manually free the memory
} // No automatic cleanup in C
return 0;
}
In C, failing to call free(s)
would result in a memory leak. Rust eliminates this risk by automatically calling drop
when variables go out of scope.
6.1.2 Ownership Transfer (Move Semantics)
When you assign or pass ownership of a heap-allocated value to another variable, Rust moves the ownership rather than copying the data. This move is the default behavior for types that do not implement the Copy
trait, and it helps prevent data races and dangling pointers by ensuring only one owner of the data exists at a time.
Rust Code
fn main() { let s1 = String::from("hello"); let s2 = s1; // s1 is moved to s2 // println!("{}", s1); // Error: s1 is no longer valid println!("{}", s2); // Outputs: hello }
After moving s1
to s2
, s1
is invalidated. Attempting to use s1
results in a compile-time error, preventing issues like double frees. This is different from a shallow copy in C, where both variables might point to the same memory location.
Comparison with C
#include <stdlib.h>
#include <string.h>
int main() {
char *s1 = malloc(6);
strcpy(s1, "hello");
char *s2 = s1; // Both s1 and s2 point to the same memory
free(s1);
// Using s2 here would be undefined behavior
return 0;
}
In C, both s1
and s2
point to the same memory. Freeing s1
and then using s2
leads to undefined behavior. Rust prevents this by invalidating s1
after the move.