所有权与内存模型
本章目标: 深入理解 Rust 所有权系统和内存布局,掌握 Move/Copy 语义
学习时间: 5-7 天
前置知识: 完成
01-从Go到Rust-思维转换.md
所有权: Rust 的核心创新
所有权系统是 Rust 区别于所有其他语言的核心特性。理解它,你就理解了 Rust。
三条所有权规则
规则 1: Rust 中每个值都有一个变量作为它的所有者
规则 2: 每个值在任一时刻只能有一个所有者
规则 3: 当所有者离开作用域,值会被立即释放(drop)
这三条规则看似简单,却是整个内存安全保证的基础。
1. 内存布局: 栈与堆
栈内存 (Stack)
fn stack_example() {
let x: i32 = 42; // 栈上分配, 4 字节
let y: bool = true; // 栈上分配, 1 字节
let z: f64 = 3.14; // 栈上分配, 8 字节
// 函数返回时,x/y/z 立即释放
} // ← 栈指针回退, 内存瞬间回收栈的特点:
- 分配和释放极快(移动栈指针)
- 大小必须在编译期已知
- 后进先出(LIFO)
- 线程专属,无同步开销
堆内存 (Heap)
fn heap_example() {
let s = String::from("hello"); // 堆上分配
let v = vec![1, 2, 3]; // 堆上分配
// s 和 v 在栈上各有一个"胖指针"
// 指向堆上的实际数据
} // ← s 和 v 离开作用域, drop 被调用, 堆内存释放堆的特点:
- 大小可以在运行时确定
- 分配/释放较慢(需要内存分配器)
- 通过指针访问
Rust 内存布局图解
栈 (Stack) 堆 (Heap)
┌──────────────────┐ ┌─────────────────┐
│ s: String │ │ h e l l o │
│ ┌─────────────┐ │ │ (5 bytes) │
│ │ ptr ────────┼─┼─────►│ │
│ │ len: 5 │ │ │ │
│ │ cap: 5 │ │ │ │
│ └─────────────┘ │ └─────────────────┘
│ │
│ x: i32 = 42 │ (整数直接在栈上)
└──────────────────┘
哪些类型分配在栈上?
// 栈分配类型
let a: i32 = 42; // 整数
let b: f64 = 3.14; // 浮点数
let c: bool = true; // 布尔值
let d: char = 'A'; // 字符
let e: (i32, bool) = (1, true); // 元组(如果元素都是栈类型)
let f: [i32; 3] = [1, 2, 3]; // 固定大小数组
// 堆分配类型
let s = String::from("hello"); // 字符串
let v = vec![1, 2, 3]; // 动态数组
let b = Box::new(42); // Box 包装2. 所有权与 Move 语义
Move 的本质
fn main() {
let s1 = String::from("hello");
// 内存状态:
// s1: ptr → "hello" (堆上)
let s2 = s1; // Move: s1 的所有权转移到 s2
// 内存状态:
// s1: 无效 (编译器记录)
// s2: ptr → "hello" (堆上)
// println!("{}", s1); // 编译错误!
println!("{}", s2); // 正常
}Move 发生的场景:
- 赋值:
let s2 = s1; - 函数参数:
take_ownership(s1); - 函数返回:
return s1; - 集合插入:
vec.push(s1);
为什么需要 Move?
考虑如果没有 Move 会发生什么:
// 假设赋值是"浅拷贝" (Rust 不这样做)
fn hypothetical_shallow_copy() {
let s1 = String::from("hello");
let s2 = s1; // 假设只复制指针
// 此时 s1 和 s2 的 ptr 指向同一个堆内存!
// 作用域结束时:
// s2 被 drop, 释放堆内存
// s1 被 drop, 再次释放同一堆内存 ← 双重释放 bug!
}Move 语义通过转移所有权,确保每块内存只有一个所有者,从根本上避免了双重释放。
函数与所有权
fn takes_ownership(some_string: String) {
// some_string 进入作用域
println!("{}", some_string);
} // some_string 离开作用域, drop 被调用
fn makes_copy(some_integer: i32) {
// some_integer 进入作用域
println!("{}", some_integer);
} // some_integer 离开作用域,但 i32 是 Copy 的,无 drop
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string // 移动到调用方
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 接收并原样返回
}
fn main() {
let s1 = gives_ownership(); // 接收所有权
let s2 = String::from("world");
let s3 = takes_and_gives_back(s2); // s2 moved, s3 得到所有权
takes_ownership(s1); // s1 的所有权进入函数
// println!("{}", s1); // 错误!
let x = 5;
makes_copy(x); // i32 是 Copy 类型
println!("{}", x); // 正常!
}3. Copy 语义
Copy Trait
有些类型实现了 Copy trait。复制这些类型时,原变量仍然有效:
fn main() {
let x = 5;
let y = x; // Copy: 在栈上复制了一份
// x 和 y 都可以使用
println!("x = {}, y = {}", x, y);
}哪些类型实现了 Copy?
// 实现 Copy 的类型:
let a: i8 = 1; // 所有整数类型 i8/i16/i32/i64/i128/u8...
let b: f32 = 1.0; // 浮点类型 f32/f64
let c: bool = true; // bool
let d: char = 'a'; // char
let e: () = (); // 单元类型
// 如果元素实现了 Copy, 固定大小数组和元组也是 Copy
let f: [i32; 3] = [1, 2, 3]; // Copy
let g: (i32, bool) = (1, true); // Copy
// 没有实现 Copy 的类型:
// String, Vec<T>, Box<T>, 以及所有包含堆分配的类型自定义 Copy 类型
// 使用 derive 宏实现 Copy (需要所有字段都实现 Copy)
#[derive(Clone, Copy, Debug)]
struct Point {
x: f64,
y: f64,
}
fn main() {
let p1 = Point { x: 1.0, y: 2.0 };
let p2 = p1; // Copy, 不是 Move
println!("p1 = {:?}, p2 = {:?}", p1, p2); // 两者都可用
}注意: 实现 Copy 的类型必须也实现 Clone,且不能包含任何需要 Drop 的字段。
4. Drop Trait 与 RAII
RAII 模式
RAII (Resource Acquisition Is Initialization) 是 Rust 内存管理的核心模式:
fn raii_demo() {
let f = File::open("hello.txt").unwrap(); // 资源获取
// 使用 f...
// 作用域结束时, f 自动 drop, 文件句柄自动关闭
} // ← Drop 在此处自动调用实现 Drop Trait
struct Database {
name: String,
connection_id: u32,
}
impl Drop for Database {
fn drop(&mut self) {
println!("关闭数据库连接 {} (id: {})",
self.name, self.connection_id);
// 实际项目中这里会清理资源
}
}
fn main() {
let db1 = Database {
name: String::from("users"),
connection_id: 1
};
{
let db2 = Database {
name: String::from("orders"),
connection_id: 2
};
println!("使用 db2");
} // ← db2 在此 drop
println!("db1 还在使用");
} // ← db1 在此 drop输出:
使用 db2
关闭数据库连接 orders (id: 2)
db1 还在使用
关闭数据库连接 users (id: 1)
Drop 顺序
struct Foo(i32);
impl Drop for Foo {
fn drop(&mut self) {
println!("Dropping Foo({})", self.0);
}
}
fn main() {
let _x = Foo(1);
let _y = Foo(2);
let _z = Foo(3);
}
// 输出 (逆序 drop):
// Dropping Foo(3)
// Dropping Foo(2)
// Dropping Foo(1)规则: 变量按声明顺序的逆序 drop。结构体字段按声明顺序 drop。
提前 drop
fn main() {
let mut db = connect_to_database();
{
// 使用 db...
}
// 此时 db 还未 drop!
drop(db); // 手动提前 drop
// 现在可以做其他独占操作了
do_exclusive_operation();
}5. 所有权与函数: 深入分析
高效传递所有权
// 低效: 不必要地移动然后返回
fn bad_append(mut s: String) -> String {
s.push_str(" world");
s // 必须返回才能继续使用
}
// 高效: 使用借用
fn good_append(s: &mut String) {
s.push_str(" world");
// 不需要返回
}
// 最惯用: 参数和返回值都是值类型
fn transform(s: String) -> String {
format!("{} world", s)
}所有权与集合
fn ownership_with_collections() {
let mut v: Vec<String> = Vec::new();
let s1 = String::from("hello");
v.push(s1); // s1 moved 进 v
// println!("{}", s1); // 错误!
// 从集合中取出元素
let s2 = v.remove(0); // 从 v 中 move 出来
println!("{}", s2); // 正常
// 迭代消费集合
let v2 = vec![
String::from("a"),
String::from("b"),
String::from("c"),
];
for s in v2 { // v2 被消费
println!("{}", s);
}
// v2 不再可用
}6. 内存安全保证
Rust 防止的内存错误
// 1. 使用后释放 (Use After Free) - Rust 通过所有权防止
fn use_after_free_prevented() {
let x = Box::new(42);
let y = x; // x moved
// println!("{}", *x); // 编译错误! 而非运行时崩溃
println!("{}", *y);
}
// 2. 双重释放 (Double Free) - 单一所有者保证不会双重释放
fn double_free_prevented() {
let s = String::from("test");
let t = s; // 所有权转移
// s 会被 drop 吗? 不会! 编译器知道 s 已移动
}
// 3. 内存泄漏 (Memory Leak) - RAII 自动释放
fn no_memory_leak() {
let s = String::from("will be freed");
// 即使 panic 发生, drop 也会被调用 (unwinding)
} // s 在此自动释放
// 4. 悬垂指针 (Dangling Pointer) - 生命周期系统防止
fn dangling_pointer_prevented() -> &str {
let s = String::from("hello");
// &s // 编译错误! s 的生命周期在函数结束时结束
"static string" // 这是安全的
}7. 所有权与性能
零成本所有权
// Rust 的所有权转移在运行时只是移动几个字节(指针)
fn move_is_zero_cost(data: Vec<u8>) -> Vec<u8> {
// 移动 Vec (3 个 usize: ptr, len, cap)
// 不复制任何实际数据!
data
}
// 对比 Go 的 GC:
// - Go 需要 GC 扫描和暂停
// - Rust 在编译期确定释放点, 零运行时开销栈 vs 堆分配
// 栈分配: 快速
fn stack_alloc() {
for _ in 0..1_000_000 {
let x: [i32; 100] = [0; 100]; // 栈上, 不需要分配器
let _ = x;
}
}
// 堆分配: 较慢
fn heap_alloc() {
for _ in 0..1_000_000 {
let v: Vec<i32> = vec![0; 100]; // 堆上, 需要 malloc/free
let _ = v;
}
}8. 实战: 实现一个简单的内存池
理解所有权后,我们来实现一个简单的内存池:
/// 简单的连接池示例
pub struct ConnectionPool {
available: Vec<Connection>,
max_size: usize,
}
pub struct Connection {
id: u32,
query_count: u32,
}
impl Connection {
fn new(id: u32) -> Self {
println!("创建连接 {}", id);
Connection { id, query_count: 0 }
}
pub fn query(&mut self, sql: &str) -> String {
self.query_count += 1;
format!("连接 {} 执行查询 {}: {}", self.id, self.query_count, sql)
}
}
impl Drop for Connection {
fn drop(&mut self) {
println!("关闭连接 {} (执行了 {} 次查询)",
self.id, self.query_count);
}
}
impl ConnectionPool {
pub fn new(size: usize) -> Self {
let available = (0..size)
.map(|i| Connection::new(i as u32))
.collect();
ConnectionPool { available, max_size: size }
}
pub fn get(&mut self) -> Option<Connection> {
self.available.pop()
}
pub fn put(&mut self, conn: Connection) {
if self.available.len() < self.max_size {
self.available.push(conn);
}
// 如果池已满, conn 在此 drop (连接关闭)
}
}
fn main() {
let mut pool = ConnectionPool::new(3);
// 取出一个连接
if let Some(mut conn) = pool.get() {
println!("{}", conn.query("SELECT * FROM users"));
pool.put(conn); // 归还连接
}
// 用完连接不归还
if let Some(mut conn) = pool.get() {
println!("{}", conn.query("SELECT * FROM orders"));
// conn 在这里 drop (超出作用域)
}
println!("程序结束");
}
// 所有剩余连接在 pool drop 时关闭练习题
练习 1 (基础): 追踪所有权
给以下代码注释 “move” 或 “copy”:
fn main() {
let x = 42;
let y = x; // ___?
let s1 = String::from("hi");
let s2 = s1; // ___?
let p = (1i32, 2i32);
let q = p; // ___?
let v1 = vec![1,2,3];
let v2 = v1; // ___?
}答案:
y = x → copy (i32 实现 Copy)
s2 = s1 → move (String 不实现 Copy)
q = p → copy (元组内都是 i32, 实现 Copy)
v2 = v1 → move (Vec 不实现 Copy)
练习 2 (基础): 修复所有权错误
fn main() {
let s = String::from("hello");
let r = first_word(&s);
s.clear(); // 问题在哪?
println!("{}", r);
}
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b == b' ' {
return &s[..i];
}
}
&s[..]
}答案: s.clear() 会修改 s,但 r 持有对 s 的不可变借用。解决方法:
fn main() {
let s = String::from("hello world");
let r = first_word(&s);
println!("{}", r); // 先使用 r
// r 的生命周期在这里结束
// 注意: 如果需要 clear(), 用 s.to_string() 先把 r 变成 String
// let r = first_word(&s).to_string();
// s.clear();
// println!("{}", r);
}练习 3 (中等): 实现 Drop
实现一个 TempFile 类型,在 drop 时自动删除文件:
use std::fs;
use std::path::PathBuf;
struct TempFile {
path: PathBuf,
}
impl TempFile {
fn new(path: &str) -> Self {
fs::write(path, "temp content").unwrap();
TempFile { path: PathBuf::from(path) }
}
fn path(&self) -> &str {
self.path.to_str().unwrap()
}
}
impl Drop for TempFile {
fn drop(&mut self) {
if self.path.exists() {
fs::remove_file(&self.path).unwrap_or_default();
println!("已删除临时文件: {:?}", self.path);
}
}
}
fn main() {
{
let tmp = TempFile::new("/tmp/test_temp_file.txt");
println!("创建了: {}", tmp.path());
// 使用文件...
} // tmp 在这里 drop, 文件被删除
println!("文件应该已被删除");
}练习 4 (中等): 所有权与集合
// 修复这段代码使其能输出所有字符串
fn main() {
let strings = vec![
String::from("alpha"),
String::from("beta"),
String::from("gamma"),
];
for s in strings {
println!("{}", s);
}
// 问题: strings 已被消费, 这里还能使用吗?
println!("总数: {}", strings.len()); // 如何修复?
}三种修复方法:
// 方法 1: 先记长度
let len = strings.len();
for s in strings {
println!("{}", s);
}
println!("总数: {}", len);
// 方法 2: 借用迭代
for s in &strings {
println!("{}", s);
}
println!("总数: {}", strings.len());
// 方法 3: 克隆
let strings2 = strings.clone();
for s in strings {
println!("{}", s);
}
println!("总数: {}", strings2.len());练习 5 (中等): 函数所有权转移
实现一个函数 most_common_char,找出字符串中出现次数最多的字符。注意所有权的正确转移:
use std::collections::HashMap;
fn most_common_char(s: String) -> (char, usize) {
let mut counts: HashMap<char, usize> = HashMap::new();
for c in s.chars() {
*counts.entry(c).or_insert(0) += 1;
}
counts
.into_iter()
.max_by_key(|(_, count)| *count)
.unwrap_or(('\0', 0))
}
fn main() {
let text = String::from("hello world");
let (c, count) = most_common_char(text);
println!("最常见字符: '{}' 出现 {} 次", c, count);
}练习 6 (挑战): 实现 RAII 锁
实现一个简单的互斥锁,使用 RAII 确保锁总被释放:
use std::cell::UnsafeCell;
use std::sync::atomic::{AtomicBool, Ordering};
pub struct SimpleMutex<T> {
locked: AtomicBool,
data: UnsafeCell<T>,
}
pub struct MutexGuard<'a, T> {
mutex: &'a SimpleMutex<T>,
}
unsafe impl<T: Send> Send for SimpleMutex<T> {}
unsafe impl<T: Send> Sync for SimpleMutex<T> {}
impl<T> SimpleMutex<T> {
pub fn new(data: T) -> Self {
SimpleMutex {
locked: AtomicBool::new(false),
data: UnsafeCell::new(data),
}
}
pub fn lock(&self) -> MutexGuard<'_, T> {
// 自旋等待
while self.locked.compare_exchange(
false, true,
Ordering::Acquire, Ordering::Relaxed
).is_err() {
std::hint::spin_loop();
}
MutexGuard { mutex: self }
}
}
impl<T> std::ops::Deref for MutexGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
unsafe { &*self.mutex.data.get() }
}
}
impl<T> std::ops::DerefMut for MutexGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
unsafe { &mut *self.mutex.data.get() }
}
}
impl<T> Drop for MutexGuard<'_, T> {
fn drop(&mut self) {
self.mutex.locked.store(false, Ordering::Release);
println!("锁已释放");
}
}
fn main() {
let mutex = SimpleMutex::new(0i32);
{
let mut guard = mutex.lock();
println!("获得锁, 值 = {}", *guard);
*guard = 42;
println!("修改值为 = {}", *guard);
} // guard drop, 锁自动释放
let guard = mutex.lock();
println!("再次获得锁, 值 = {}", *guard);
}练习 7 (挑战): 内存布局分析
分析以下结构体在内存中的布局:
use std::mem;
struct Small {
a: u8,
b: u8,
}
struct Padded {
a: u8,
b: u32, // 注意对齐
c: u8,
}
#[repr(C)]
struct PackedC {
a: u8,
b: u32,
c: u8,
}
fn main() {
println!("Small size: {} bytes", mem::size_of::<Small>());
println!("Padded size: {} bytes", mem::size_of::<Padded>());
println!("PackedC size: {} bytes", mem::size_of::<PackedC>());
// Small: 2 bytes (u8 + u8, 无填充)
// Padded: 12 bytes (u8(1) + 填充(3) + u32(4) + u8(1) + 填充(3))
// PackedC: 9 bytes (#[repr(C)] 按 C 标准布局)
}练习 8 (挑战): 自定义 Drop 顺序
理解并修复以下代码的 drop 顺序问题:
struct Logger {
name: &'static str,
}
impl Drop for Logger {
fn drop(&mut self) {
println!("Logger '{}' dropped", self.name);
}
}
struct Service {
logger: Logger,
name: &'static str,
}
impl Drop for Service {
fn drop(&mut self) {
// 问题: 此时 self.logger 还存在吗?
println!("Service '{}' shutting down", self.name);
// 可以在这里使用 self.logger
}
}
fn main() {
let s = Service {
name: "web",
logger: Logger { name: "web-logger" },
};
println!("Service running");
}
// 期望输出顺序是什么?答案:
Service running
Service 'web' shutting down ← impl Drop 先运行
Logger 'web-logger' dropped ← 然后字段按声明顺序 drop
结构体的 drop() 方法在字段被 drop 之前调用,所以可以在 drop() 中使用字段。
练习 9 (中等): 所有权与枚举
实现一个表示计算结果的枚举,包含值或错误:
#[derive(Debug)]
enum Computation {
Value(f64),
Error(String),
Pending,
}
impl Computation {
fn compute(input: f64) -> Self {
if input < 0.0 {
Computation::Error(format!("负数不支持: {}", input))
} else if input.is_nan() {
Computation::Pending
} else {
Computation::Value(input.sqrt())
}
}
fn unwrap_or(self, default: f64) -> f64 {
match self {
Computation::Value(v) => v,
_ => default,
}
}
}
fn main() {
let results: Vec<Computation> = vec![-1.0, 4.0, 9.0, 16.0]
.into_iter()
.map(Computation::compute)
.collect();
for result in results {
println!("{:?}", result);
}
}练习 10 (综合): 文件处理器
实现一个文件处理器,展示所有权在实际场景中的应用:
use std::fs;
use std::io::{self, Read, Write};
use std::path::Path;
pub struct FileProcessor {
input_path: String,
output_path: String,
}
impl FileProcessor {
pub fn new(input: &str, output: &str) -> Self {
FileProcessor {
input_path: input.to_string(),
output_path: output.to_string(),
}
}
pub fn process<F>(&self, transform: F) -> io::Result<usize>
where
F: Fn(String) -> String,
{
// 读取输入文件(获得 String 所有权)
let content = fs::read_to_string(&self.input_path)?;
// 转换内容(消费 content, 产生新 String)
let transformed = transform(content);
// 写入输出文件
let bytes_written = transformed.len();
fs::write(&self.output_path, transformed)?;
Ok(bytes_written)
}
}
fn main() -> io::Result<()> {
// 写入测试文件
fs::write("/tmp/input.txt", "hello world\nfoo bar\n")?;
let processor = FileProcessor::new("/tmp/input.txt", "/tmp/output.txt");
let bytes = processor.process(|content| {
content.to_uppercase()
})?;
println!("处理了 {} 字节", bytes);
println!("结果: {}", fs::read_to_string("/tmp/output.txt")?);
Ok(())
}源码导读
标准库中的 Drop
查看 std::collections::vec_deque::VecDeque 的 drop 实现,理解复杂数据结构如何管理内存:
// 在 Rust 源码中 (简化版)
impl<T, A: Allocator> Drop for VecDeque<T, A> {
fn drop(&mut self) {
// 1. 安全地 drop 所有元素
struct Dropper<'a, T, A: Allocator>(&'a mut VecDeque<T, A>);
impl<T, A: Allocator> Drop for Dropper<'_, T, A> {
fn drop(&mut self) {
// 即使 panic, 也确保释放内存
unsafe { ... }
}
}
// 2. 释放底层内存
}
}重点理解:
- 为什么内部用
Dropper结构体? (处理 panic 安全性) drop_in_place和drop的区别- allocator 接口的作用
小结
| 概念 | 要点 |
|---|---|
| 栈 vs 堆 | 栈快但大小固定,堆灵活但需管理 |
| 所有权规则 | 一个值一个所有者,离开作用域自动释放 |
| Move 语义 | 转移所有权,原变量失效 |
| Copy 语义 | 类型可 Copy 时,赋值复制而非移动 |
| Drop/RAII | 离开作用域自动清理,类似 Go 的 defer+close |
下一步
- 完成本章所有练习题
- 阅读
03-借用检查器原理.md - 在 Rust Playground 中实验 drop 顺序
记住: 所有权系统让 Rust 成为”编译器是最严格的代码审查员”的语言! 🦀