所有权基础
> Rust 最独特的特性,内存安全的核心保障——无需垃圾回收器
本章目标
完成本章学习后,你将能够:
- 理解为什么 Rust 需要所有权系统
- 掌握所有权的三大规则
- 理解作用域与内存释放的关系
- 区分栈内存和堆内存的使用场景
核心概念
栈内存与堆内存
在深入所有权之前,必须理解 Rust 程序如何使用内存。
为什么需要两种内存?
┌─────────────────────────────────────────────────────────────┐
│ 程序运行时的内存布局 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 高地址 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 栈(Stack) │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ 函数调用栈帧 │ ↓ 增长 │
│ │ │ - 局部变量 │ │ │
│ │ │ - 函数参数 │ │ │
│ │ │ - 返回地址 │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ ├─────────────────────────────────────────────┤ │
│ │ │ │
│ │ ↓↓↓ 空闲内存 ↓↓↓ │ │
│ │ │ │
│ ├─────────────────────────────────────────────┤ │
│ │ 堆(Heap) │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ 动态分配的数据 │ ↑ 增长 │
│ │ │ - Box::new() │ │ │
│ │ │ - String │ │ │
│ │ │ - Vec<T> │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────┘ │
│ 低地址 │
│ │
└─────────────────────────────────────────────────────────────┘栈 vs 堆:详细对比
┌─────────────────────────────────────────────────────────────┐
│ 栈(Stack)vs 堆(Heap)详细对比 │
├──────────────────┬────────────────────┬─────────────────────┤
│ 特性 │ 栈 │ 堆 │
├──────────────────┼────────────────────┼─────────────────────┤
│ 分配方式 │ 编译时确定大小 │ 运行时动态分配 │
│ 分配速度 │ 极快(移动指针) │ 较慢(搜索空闲块) │
│ 访问方式 │ 直接访问 │ 通过指针访问 │
│ 大小限制 │ 通常较小(MB级) │ 受系统内存限制 │
│ 生命周期 │ 自动管理(LIFO) │ 手动/自动管理 │
│ 内存碎片 │ 无 │ 可能产生碎片 │
│ 缓存友好性 │ 高(连续内存) │ 较低(分散内存) │
├──────────────────┼────────────────────┼─────────────────────┤
│ 存储内容 │ • 基本类型 │ • String │
│ │ • 固定大小数组 │ • Vec<T> │
│ │ • 指针 │ • Box<T> │
│ │ • 函数参数 │ • HashMap │
│ │ • 局部变量 │ • 动态大小数据 │
├──────────────────┼────────────────────┼─────────────────────┤
│ 示例 │ let x: i32 = 5; │ let s = String:: │
│ │ let arr = [1,2,3]; │ from("hello"); │
└──────────────────┴────────────────────┴─────────────────────┘栈内存分配过程
fn main() {
let x = 5; // ①
let y = 10; // ②
{
let z = 15; // ③
} // ④ z 被释放
} // ⑤ y, x 被释放
栈内存变化过程:
① 分配 x 后:
┌─────────────┐
│ x = 5 │ ← 栈顶
└─────────────┘
② 分配 y 后:
┌─────────────┐
│ y = 10 │ ← 栈顶
├─────────────┤
│ x = 5 │
└─────────────┘
③ 分配 z 后:
┌─────────────┐
│ z = 15 │ ← 栈顶
├─────────────┤
│ y = 10 │
├─────────────┤
│ x = 5 │
└─────────────┘
④ z 离开作用域:
┌─────────────┐
│ y = 10 │ ← 栈顶
├─────────────┤
│ x = 5 │
└─────────────┘
⑤ main 结束:
┌─────────────┐
│ (空) │ ← 栈顶
└─────────────┘堆内存分配过程
let s = String::from("hello");
堆分配过程:
步骤 1:请求分配
┌─────────────────────────────────────────────────────┐
│ 内存分配器在堆中查找足够大的空闲块 │
│ │
│ 堆内存: │
│ ┌──────┬──────┬──────┬──────┬──────┬────────────┐ │
│ │ 已用 │ 已用 │ 空闲 │ 空闲 │ 空闲 │ ... │ │
│ └──────┴──────┴──────┴──────┴──────┴────────────┘ │
│ ↑ │
│ 找到空闲块 │
└─────────────────────────────────────────────────────┘
步骤 2:写入数据
┌─────────────────────────────────────────────────────┐
│ 在找到的位置写入 "hello" │
│ │
│ 堆内存: │
│ ┌──────┬──────┬──────┬──────┬──────┬────────────┐ │
│ │ 已用 │ 已用 │ 'h' │ 'e' │ 'l' │ 'l' │'o' │ │
│ └──────┴──────┴──────┴──────┴──────┴──────┴─────┘ │
│ ↑ │
│ 地址 0x1000(示例) │
└─────────────────────────────────────────────────────┘
步骤 3:栈上存储元数据
┌─────────────────────────────────────────────────────┐
│ 栈上创建 String 结构体 │
│ │
│ 栈: │
│ ┌────────────────────────────────┐ │
│ │ s │ │
│ ├────────────────┬───────────────┤ │
│ │ ptr │ 0x1000 ────────┼──┐ │
│ │ len │ 5 │ │ │
│ │ capacity │ 5 │ │ │
│ └────────────────┴────────────────┘ │ │
│ ▼ │
│ 堆: ┌─────┐ │
│ │'h' │ │
│ │'e' │ │
│ │'l' │ │
│ │'l' │ │
│ │'o' │ │
│ └─────┘ │
└─────────────────────────────────────────────────────┘所有权的三大规则
为什么需要所有权?
┌─────────────────────────────────────────────────────┐
│ 内存管理方式对比 │
├─────────────────────────────────────────────────────┤
│ │
│ C/C++:手动管理 │
│ ├── 程序员负责分配/释放 │
│ ├── 灵活但容易出错 │
│ └── 常见问题: │
│ • 内存泄漏:忘记释放 │
│ • 悬垂指针:释放后继续使用 │
│ • 双重释放:同一内存释放两次 │
│ • 使用后释放:访问已释放的内存 │
│ │
│ Java/Python/Go:垃圾回收(GC) │
│ ├── 运行时自动回收 │
│ ├── 安全但有性能开销 │
│ └── 问题: │
│ • GC 暂停:程序停止进行垃圾回收 │
│ • 不可预测延迟:GC 时机不确定 │
│ • 内存占用高:需要预留 GC 工作内存 │
│ • 实时性差:不适合实时系统 │
│ │
│ Rust:所有权系统 │
│ ├── 编译时检查 │
│ ├── 零运行时开销 │
│ └── 安全且高效! │
│ • 编译期保证内存安全 │
│ • 无 GC 暂停 │
│ • 可预测的性能 │
│ • 适合系统编程和实时系统 │
│ │
└─────────────────────────────────────────────────────┘C++ 内存安全问题示例
cpp
// C++ 内存泄漏示例
void create_leak() {
char* buffer = new char[100];
// 忘记 delete,内存泄漏!
// 程序每次调用都会泄漏 100 字节
}
// C++ 悬垂指针示例
char* create_dangling() {
char buffer[10] = "hello";
return buffer; // 返回局部变量的地址
// buffer 在函数返回后被释放
// 返回的指针指向已释放的内存
}
// C++ 双重释放示例
void double_free() {
char* p = new char[10];
delete[] p;
delete[] p; // 双重释放!程序崩溃或数据损坏
}
// C++ 使用后释放示例
void use_after_free() {
char* p = new char[10];
delete[] p;
p[0] = 'a'; // 写入已释放的内存
// 可能导致程序崩溃或数据损坏
}Rust 的解决方案
rust
▶ Runfn no_leak() {
let buffer = String::with_capacity(100);
// 离开作用域时自动释放!
// 编译器在作用域结尾插入 drop 调用
}
fn main() {
no_leak();
// buffer 内存已正确释放
}所有权的三大定律
┌─────────────────────────────────────────────────────┐
│ 所有权三定律 │
├─────────────────────────────────────────────────────┤
│ │
│ 第一定律:每个值都有一个所有者 │
│ Each value has an owner │
│ │
│ 第二定律:任一时刻只能有一个所有者 │
│ There can only be one owner at a time │
│ │
│ 第三定律:当所有者离开作用域,值会被丢弃 │
│ When the owner goes out of scope, │
│ the value will be dropped │
│ │
└─────────────────────────────────────────────────────┘定律详解与图解
定律 1:每个值都有一个所有者
let s = String::from("hello");
┌─────────────────────────────────────────────────────┐
│ 变量 s 是值 "hello" 的唯一所有者 │
│ │
│ 栈: │
│ ┌────────────────────────────────┐ │
│ │ s (所有者) │ │
│ ├────────────────┬───────────────┤ │
│ │ ptr │ 0x1000 ────────┼──┐ │
│ │ len │ 5 │ │ │
│ │ capacity │ 5 │ │ │
│ └────────────────┴────────────────┘ │ │
│ ▼ │
│ 堆: ┌─────┐ │
│ │"hello"│ │
│ └─────┘ │
│ │
│ 所有权关系:s ──拥有──▶ "hello" │
└─────────────────────────────────────────────────────┘定律 2:任一时刻只能有一个所有者
let s1 = String::from("hello");
let s2 = s1; // 所有权转移
移动前:
┌─────────────────────────────────────┐
│ 栈 堆 │
│ ┌──────┐ ┌─────────┐ │
│ │ s1 │ ───────▶│ "hello" │ │
│ └──────┘ └─────────┘ │
└─────────────────────────────────────┘
移动后(所有权转移):
┌─────────────────────────────────────┐
│ 栈 堆 │
│ ┌──────┐ ┌─────────┐ │
│ │ s1 │ (无效) │ "hello" │ │
│ ├──────┤ │ │ │
│ │ s2 │ ───────▶│ │ │
│ └──────┘ └─────────┘ │
└─────────────────────────────────────┘
关键点:
• s2 现在是唯一的所有者
• s1 变成无效(编译器保证)
• 数据没有移动,只是所有权转移定律 3:所有者离开作用域,值被丢弃
{
let s = String::from("hello");
// s 从这里开始有效
} // s 离开作用域
// Rust 自动调用 drop(s)
// 堆内存被释放
作用域内:
┌─────────────────────────────────────┐
│ 作用域开始 { │
│ let s = String::from("hello"); │
│ // s 有效 │
│ println!("{}", s); │
│ } ← 作用域结束 │
│ ↓ │
│ Rust 调用:drop(s) │
│ 堆内存被释放 │
└─────────────────────────────────────┘
作用域结束后:
┌─────────────────────────────────────┐
│ 栈: │
│ (s 已被释放) │
│ │
│ 堆: │
│ ("hello" 已被释放) │
└─────────────────────────────────────┘规则验证代码
rust
▶ Runfn main() {
println!("=== 定律 1:每个值都有一个所有者 ===");
// s 是 "hello" 的唯一所有者
let s = String::from("hello");
println!("所有者 s: {}", s);
println!("\n=== 定律 2:任一时刻只能有一个所有者 ===");
let s1 = String::from("world");
let s2 = s1; // 所有权从 s1 移动到 s2
// println!("s1: {}", s1); // ❌ 错误:s1 已无效
println!("s2: {}", s2); // ✅ 正确:s2 是所有者
println!("\n=== 定律 3:作用域结束自动释放 ===");
{
let inner = String::from("inner scope");
println!("inner: {}", inner);
} // inner 在这里被释放
// println!("inner: {}", inner); // ❌ 错误:inner 不存在
println!("inner 已离开作用域");
}规则记忆口诀
一值一主,时刻唯一
主离作用域,值即丢弃实战案例
案例 1:内存泄漏对比
问题描述
演示 Rust 如何防止内存泄漏,对比 C++ 的手动管理。
C++ 版本(内存泄漏风险)
cpp
#include <iostream>
#include <cstring>
// C++ 容易发生内存泄漏
void process_data() {
char* buffer = new char[1000];
// 如果这里抛出异常
if (true) { // 模拟异常条件
throw std::runtime_error("Error occurred");
}
// 这行永远不会执行
delete[] buffer;
}
int main() {
try {
process_data();
} catch (const std::exception& e) {
std::cout << "Exception: " << e.what() << std::endl;
// buffer 内存泄漏!
}
return 0;
}Rust 版本(自动清理)
rust
▶ Runfn process_data() -> Result<(), String> {
let buffer = String::with_capacity(1000);
// 即使这里返回错误
if true { // 模拟错误条件
return Err("Error occurred".to_string());
}
// 这行永远不会执行
// 但 buffer 会自动清理
Ok(())
}
fn main() {
match process_data() {
Ok(()) => println!("Success"),
Err(e) => println!("Error: {}", e),
}
// buffer 内存已正确释放,无泄漏!
}运行结果
Error: Error occurred关键点:Rust 的 RAII(Resource Acquisition Is Initialization)保证即使发生错误,资源也会自动清理。
案例 2:嵌套作用域
问题描述
理解变量的生命周期与作用域的关系。
代码实现
rust
▶ Runfn main() {
println!("=== 外层作用域开始 ===");
let outer = String::from("外层");
println!("创建 outer: {}", outer);
{
println!("\n--- 内层作用域开始 ---");
let inner = String::from("内层");
println!("创建 inner: {}", inner);
// 可以访问外层变量
println!("内层访问 outer: {}", outer);
println!("内层访问 inner: {}", inner);
println!("--- 内层作用域结束 ---");
} // inner 在这里被释放
println!("\n=== 回到外层作用域 ===");
println!("outer 仍然有效: {}", outer);
// println!("inner: {}", inner); // ❌ 错误:inner 不存在
}运行结果
=== 外层作用域开始 ===
创建 outer: 外层
--- 内层作用域开始 ---
创建 inner: 内层
内层访问 outer: 外层
内层访问 inner: 内层
--- 内层作用域结束 ---
=== 回到外层作用域 ===
outer 仍然有效: 外层内存布局变化
外层作用域:
┌─────────────────┐
│ outer: "外层" │
└─────────────────┘
内层作用域:
┌─────────────────┐
│ inner: "内层" │ ← 栈顶
├─────────────────┤
│ outer: "外层" │
└─────────────────┘
内层作用域结束:
┌─────────────────┐
│ (inner 已释放) │
├─────────────────┤
│ outer: "外层" │
└─────────────────┘案例 3:释放顺序验证
问题描述
验证 Rust 中变量的释放顺序(后进先出 LIFO)。
代码实现
rust
▶ Runstruct DropTracker {
name: String,
}
impl Drop for DropTracker {
fn drop(&mut self) {
println!("🗑️ 释放: {}", self.name);
}
}
fn main() {
println!("=== 创建变量 ===");
let _a = DropTracker { name: "a".to_string() };
let _b = DropTracker { name: "b".to_string() };
let _c = DropTracker { name: "c".to_string() };
{
println!("\n--- 内层作用域 ---");
let _d = DropTracker { name: "d".to_string() };
let _e = DropTracker { name: "e".to_string() };
println!("内层作用域结束");
} // e 先释放,然后 d
println!("\n=== 主函数结束 ===");
} // c 先释放,然后 b,最后 a运行结果
=== 创建变量 ===
--- 内层作用域 ---
内层作用域结束
🗑️ 释放: e
🗑️ 释放: d
=== 主函数结束 ===
🗑️ 释放: c
🗑️ 释放: b
🗑️ 释放: a关键点:变量按照创建的相反顺序释放(LIFO)。
常见错误
错误 1:使用已移动的值
错误描述
当一个值被移动后,原变量不能再使用。
错误代码
rust
▶ Runfn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 的所有权移动到 s2
println!("{}", s1); // ❌ 错误:使用了已移动的值
}编译错误信息
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:20
|
3 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
4 | let s2 = s1;
| -- value moved here
5 | println!("{}", s1);
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value before moving it
|
4 | let s2 = s1.clone();
| +++++++++++修复方法
rust
▶ Runfn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // 显式克隆
println!("s1 = {}", s1); // ✅ 正确
println!("s2 = {}", s2); // ✅ 正确
}错误 2:变量在作用域外使用
错误描述
变量只在其定义的作用域内有效。
错误代码
rust
▶ Runfn main() {
{
let s = String::from("hello");
println!("{}", s);
} // s 在这里被释放
println!("{}", s); // ❌ 错误:s 不在作用域内
}编译错误信息
error[E0425]: cannot find value `s` in this scope
--> src/main.rs:7:20
|
7 | println!("{}", s);
| ^ not found in this scope修复方法
rust
▶ Runfn main() {
let s = String::from("hello"); // 提升到外层作用域
{
println!("内层: {}", s);
}
println!("外层: {}", s); // ✅ 正确
}错误 3:部分移动
错误描述
数组或元组的部分移动会导致整个变量失效。
错误代码
rust
▶ Runfn main() {
let tuple = (String::from("hello"), 42);
let s = tuple.0; // 移动第一个元素
// println!("{:?}", tuple); // ❌ 错误:tuple 部分移动
println!("{}", tuple.1); // ✅ 正确:第二个元素是 Copy 类型
}编译错误信息
error[E0382]: use of partially moved value: `tuple`
--> src/main.rs:6:20
|
4 | let s = tuple.0;
| ------- value partially moved here
5 |
6 | println!("{:?}", tuple);
| ^^^^^ value borrowed here after partial move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value before moving it
|
4 | let s = tuple.0.clone();
| ++++++++++++修复方法
rust
▶ Runfn main() {
let tuple = (String::from("hello"), 42);
let s = tuple.0.clone(); // 克隆而不是移动
println!("s = {}", s);
println!("tuple = {:?}", tuple); // ✅ 正确
}性能考虑
栈 vs 堆的性能差异
┌─────────────────────────────────────────────────────────┐
│ 栈 vs 堆性能对比 │
├──────────────────┬────────────────┬─────────────────────┤
│ 操作 │ 栈 │ 堆 │
├──────────────────┼────────────────┼─────────────────────┤
│ 分配速度 │ ~1 纳秒 │ ~100 纳秒 │
│ 访问速度 │ 直接访问 │ 指针间接访问 │
│ 缓存命中率 │ 高 │ 较低 │
│ 内存碎片 │ 无 │ 可能产生 │
│ 分配次数 │ 无需分配 │ 需要分配器介入 │
├──────────────────┴────────────────┴─────────────────────┤
│ │
│ 性能建议: │
│ • 优先使用栈分配的类型(Copy 类型) │
│ • 小对象优先使用栈 │
│ • 大对象或动态大小使用堆 │
│ • 减少堆分配次数(预分配、对象池) │
└─────────────────────────────────────────────────────────┘零成本抽象的含义
┌─────────────────────────────────────────────────────┐
│ 零成本抽象 │
├─────────────────────────────────────────────────────┤
│ │
│ 什么是零成本抽象? │
│ • 抽象不引入运行时开销 │
│ • 编译时代价转化为零运行时代价 │
│ • 高级特性编译后等同于手写底层代码 │
│ │
│ Rust 中的零成本抽象: │
│ • 所有权检查:编译时完成,无运行时开销 │
│ • 泛型:单态化,生成特化代码 │
│ • 迭代器:内联优化,等同于手写循环 │
│ • 智能指针:编译时优化,无额外开销 │
│ │
│ 示例:迭代器 vs 手写循环 │
│ │
│ let sum: i32 = (1..=100).sum(); │
│ // 编译后等同于: │
│ let mut sum = 0; │
│ for i in 1..=100 { sum += i; } │
│ // 无额外开销! │
│ │
└─────────────────────────────────────────────────────┘性能提示
- 优先使用栈分配:基本类型、小数组、小元组优先使用栈分配
- 预分配容量:对于 Vec、String 等集合,预分配容量减少重新分配
- 避免不必要的克隆:使用引用代替克隆,或使用
Cow类型 - 减少堆分配次数:使用对象池、arena 分配器等优化频繁分配
最佳实践
✅ 推荐做法
- 理解所有权转移:明确知道每次赋值是否发生所有权转移
- 优先使用引用:不需要所有权时,使用引用
&T - 合理使用 clone:需要独立副本时显式调用
clone() - 遵循 RAII:利用作用域自动管理资源
❌ 避免做法
- 不要过度克隆:大量克隆会严重影响性能
- 不要忽略所有权:理解所有权转移,避免编译错误
- 不要手动管理内存:让 Rust 自动处理,不要使用 unsafe
扩展阅读
官方文档
深入理解
小结
本章核心知识点
| 概念 | 关键字/语法 | 核心要点 |
|---|---|---|
| 栈内存 | 自动管理 | LIFO,快速分配,大小固定 |
| 堆内存 | Box, String, Vec | 动态分配,运行时大小 |
| 所有权三大规则 | let, move | 一值一主,时刻唯一,自动释放 |
| 作用域 | {} | 定义变量生命周期 |
学习检查清单
- [ ] 理解栈和堆的区别
- [ ] 掌握所有权三大规则
- [ ] 理解作用域与内存释放的关系
- [ ] 能够识别所有权转移的场景
- [ ] 理解 Rust 的内存安全保证
下一章
下一章将深入学习 String 类型和移动语义,理解所有权在具体类型中的表现。
➡️ String 与移动