前言
图书馆(库)无法弥补个人(程序员)能力的不足. --Mark Miller
内存管理的目标:
- 内存及时释放
- 阻止悬空指针
解决方法:
- 针对内存释放: 通过垃圾回收机制来管理内存, 所有对象的可达指针消失后, 自动释放对象. (Python, Java, C#)
- 自由优先: 让程序员自己负责释放内存和悬空指针 (C, C++)
- 限制指针: 限制使用指针的方式, 设计严格的语法让编译器可以发现内存错误, 保证内存安全(Rust)
4.1 所有权
C++内存管理
- string对象本身(栈: 自动存储持续性): 由系统管理内存 (编译后机器指令自动的
add esp, 0x10
)- 构成: 指向缓冲区的指针, capacity, length
- 字符串缓冲区(堆: 动态存储持续性): 由程序员自行管理内存, 但是存在析构函数在string对象本身被自动回收时, 同时回收缓冲区. (析构函数: 将堆内存与栈中string对象的生命周期绑定, 实现自动管理)
不安全的情况: 虽然通过析构函数可以保证对象本身的指针是安全的, 但是C++给予用户权利, 可以创建另一个指向该缓冲区的指针P
, 当内存释放时, 如果继续使用P
就会导致悬空指针.

Rust内存管理
Rust引入了所有权概念:
- 拥有者: 每个值都有决定其生命周期的唯一拥有者 (所有权代替了析构函数, 并成为值和拥有者的强制关系, 同时还有move等更多灵活的操作)
drop
: 当拥有者被释放时, 其所拥有的值也会被同时释放- 变量(自动存储管理)拥有自己的值(强制绑定生命周期), 当控制流离开声明变量的块时, 变量丢弃, 其拥有的值也一并丢弃.
fn print_padovan() {
let mut padovan = vec![1, 1, 1]; // 堆分配
for i in 3..10 {
let next = padovan[i - 3] + padovan[i - 2];
padovan.push(next);
}
println!("P(1..10) = {:?}", padovan);
} // drop
Box实例
fn main() {
let point = Box::new((0.625, 0.5)); // 分配point
let label = format!("{:?}", point); // 分配label
assert_eq!(label, "(0.625, 0.5)");
} // drop: point和label

所有权树
fn main() {
struct Person { name: String, birth: i32 }
let mut composers = Vec::new();
composers.push(Person { name: "Palestrina".to_string(),
birth: 1525 });
composers.push(Person { name: "Dowland".to_string(),
birth: 1563 });
composers.push(Person { name: "Lully".to_string(),
birth: 1632 });
for composer in &composers {
println!("{}, born {}", composer.name, composer.birth);
}
}
所有权树如下所示
所有权树如何处理: 当控制流离开composers所属作用于是, 类似于树枝的截断, composers所属的值以及子树全部drop.
单拥有者机制的影响: 将所有权的结构保持为树, 而不是图, 保证了drop机制是足够简洁的.

Rust的扩展特性
扩展特性保证了所有权具有一定的灵活性:
- 可以将值从一个拥有者
move
到另一个拥有者. (允许用户构建, 重构, 拆除树结构) - 整数, 浮点数, 字符等简单类型, 不受所有权规则约束. (Copy类型)
- 标准库提供引用计数指针类型Rc和Arc, 它们允许值在某些限制下有多个拥有者
- borrow值的引用, 这种引用是非拥有型指针, 有受限的生命周期.
4.2 移动
Rust中大多数类型的赋值操作实际上: 将值的所有者move给目标变量, 源变量回到为初始化状态, 改由目标变量来控制值的生命周期.
4.2.0.a Python的赋值操作
python使用引用计数来跟踪引用值的数量 (如下图所示)
开销问题: python需要垃圾回收机制, 来自动回收引用计数为0的值
s = ['udon', 'ramen', 'soba']
t = s
u = s

4.2.0.b C++的赋值操作
C++中vector和string赋值给其他变量的操作是生成一个副本
- 开销: 大量的内存和分配内存的处理器时间
- 优点: 三个变量均可以通过析构函数实现自动存储持续性
using namespace std;
vector<string> s = { "udon", "ramen", "soba" }; vector<string> t = s;
vector<string> u = s;

4.2.0.c Rust的赋值操作
优点: 无需复制(C++), 无需垃圾回收就能知道什么时候drop(Python).
代价: 同时访问时, 必须强制要求Copy
let s = vec!["udon".to_string(), "ramen".to_string(), "soba".to_string()];
let t = s;
let u = s; // 语法错误: s未初始化
// 显示要求Copy
let t = s.clone();
let u = s.clone();
s初始化后的内存图

move后的内存图 (源变量s变回未初始化状态)

尝试let u = s;
时报错, 因为此时s未初始化
4.2.1 更多移动操作
目标变量drop
let mut sa = "Govinda".to_string();
sa = "Siddhartha".to_string(); // drop "Govinda"
let mut sb = "Govinda".to_string();
let tb = sb; // move, sb未初始化
sb = "Siddhartha".to_string(); // 无drop
函数move
- 函数传参时会将所有权move给参数
- 函数返回时会将所有权move给调用者
- 构建元组会将所有权move给元组
4.2.2 移动与控制流
分支move限制
let xa = vec![10, 20, 30];
if c {
f(xa);
} else {
f(xa);
}
f(xa); // 编译错误: xa在分支中move
// 单分支move
let xa = vec![10, 20, 30];
if c {
f(xa);
} else {
...
}
f(xa); // 编译错误: xa在分支中move (可能move)
循环move限制
- 如果循环中不给源变量赋值, 无法move
- 如果循环中给源变量赋值, 可以move
let mut xb = vec![10];
while c {
f(xb); // move
xb = vec![10]; // 赋值
}
4.2.3 move与索引内容
如果解决下述代码有两个方法:
- 传统方法: Rust需要记忆2号元素未初始化, 通常需要一个额外的标志位, 即额外开销(Rust不能接受)
- Rust方法: 只能borrow引用, 不能获取索引内容的所有权 (Rust保证索引内容初始化)
fn main() {
let mut v = Vec::new();
for i in 101 .. 106 {
v.push(i.to_string());
}
let third = v[2]; // 编译错误, 无法移动到Vec索引结构之外
let fifth = v[4]; // 同上
}
获取索引的解决方法
/* 核心思想: 保证元素在vec中被删除, 才能转移所有权 */
let fifth = v.pop().expect("vector empty!");
assert_eq!(fifth, "105");
let second = v.swap_remove(1);
assert_eq!(second, "102");
// 替换2号元素为"substitute", replace返回元素所有权
let third = std::mem::replace(&mut v[2], "substitute".to_string);
assert_eq!(third, "103");
Vec特殊消耗元素方法
#Rust-todo #Rust-dif
- for循环move了v, v变为未初始化状态
- for循环机制获取向量所有权, 并将其分解为元素
- 所以s可以获取索引内容的所有权, 又由于s上一轮的值会被丢弃所以会消耗v中的元素.
let v = vec!["lame".to_string(), "crow".to_string(), "zry".to_string()];
for mut s in v {
s.push('!');
println!("{}", s);
}
4.2.4 move编译器无法跟踪的值
原理与索引内容类似, 二者均为编译器无法跟踪的值
struct Person { name: Option<String>, birth: i32 }
let mut composers = Vec::new();
composers.push(Person { name: Some("Palestrina".to_string()), birth: 1525 });
let first_name = composers[0].name;
但是Option<T>可以赋值为None, 通过replace()方法实现None替换, 然后move给目标变量就是合法的. (从整体看效果与move几乎一样, 只不过源变量不是未初始化的状态, 而是赋值为None. )
let first_name = std::mem::replace(&mut composers[0].name, None);
// 简化版本
let first_name = composers[0].name.take(); // 仅限于Option<T>
4.3 Copy类型: 关于移动的例外情况
- move应用场景: 设计向量, 字符串, 其他可能占用大量内存且复制成本高昂的类型
- copy应用场景: 整数, 浮点数等简单类型
Rust指定为Copy类型的类型, 对Copy类型的值进行赋值会copy, 而不是move. (函数传参和构造器同理)
常见Copy类型 | 常见非Copy类型 | 常见的规则 |
---|---|---|
1. 所有整型 2. 所有浮点型 3. char 4. bool 5. Copy类型的元组 6. 固定大小的数组 | 1. String: 因为在堆中分配了内存 2. Box<T>:同理 3. File: 代表文件句柄, 必须向OS请求, 不能复制 4. MutexGuard: 复制没有意义, 因为只能存在一个互斥锁 5. 自定义类型默认非Copy (struct, enum) | drop时需要任何特殊操作 (释放内存, 请求OS) |
fn main() {
let string1 = "lamecrow".to_string();
let string2 = string1; // move
let num1: i32 = 36;
let num2 = num1; // copy
}
内存视图

4.3.1 定义Copy类型
结构体的字段均为Copy的情况
#[derive(Copy, Clone)] // 获得Copy, Clone
struct Label { number: u32 }
fn print(l: Label) { println!("STAMP: {}", l.number); }
fn main() {
let l = Label { number: 3 };
print(l);
println!("My label number is: {}", l.number);
}
结构体的字段不均为Copy的情况, 字段不能自动转换为Copy, 因为Copy与非Copy之间的使用方式存在巨大的差异, 一旦切换将面临大量的代码修改, 所以必须将Copy实现的决定权交给开发者来决定.
#[derive(Copy, Clone)]
struct Label { number: String }
报错:
the trait `Copy` cannot be implemented for this type
4.4 Rc与Arc: 共享所有权 (引用计数指针)
针对某些需要多个拥有者的情况, Rust提供引用计数指针:
- Rc (reference count, 引用计数)
- Arc (atomic reference count, 原子引用计数)
- 二者唯一的区别: Arc可以在线程间安全共享, Rc使用更快的非线程安全代码更新其引用计数.
Rc<T>指向T型(附带引用计数)的指针, 所以clone实际上是复制指针, 指向时会修改缓冲区中的引用计数.
- Rc拥有的值不可变 (类似于读者写者问题, 多个共享指针存在时无法修改值)
use std::rc::Rc;
let s: Rc<String> = Rc::new("shirataki".to_string());
let t: Rc<String> = s.clone();
let u: Rc<String> = s.clone();

循环引用问题
a和b相互拥有, 所以二者永远不会释放, 导致了内存泄露问题.

TODO
- for循环机制问题
- 为什么for循环会move所有权
- for循环如何切割元素
- 编译器无法跟踪的值: Rust的所有权检查器borrow checker能够追踪变量的所有权和生命周期,以确保安全的内存管理。但是,在某些情况下,编译器无法静态地跟踪值的所有权状态,尤其是在可变借用和索引访问相关的场景中。
- 嵌套在结构体中的值
- 索引值