前言

图书馆(库)无法弥补个人(程序员)能力的不足. --Mark Miller

内存管理的目标:

  • 内存及时释放
  • 阻止悬空指针

解决方法:

  • 针对内存释放: 通过垃圾回收机制来管理内存, 所有对象的可达指针消失后, 自动释放对象. (Python, Java, C#)
  • 自由优先: 让程序员自己负责释放内存和悬空指针 (C, C++)
  • 限制指针: 限制使用指针的方式, 设计严格的语法让编译器可以发现内存错误, 保证内存安全(Rust)

4.1 所有权

C++内存管理

  • string对象本身(栈: 自动存储持续性): 由系统管理内存 (编译后机器指令自动的add esp, 0x10)
    • 构成: 指向缓冲区的指针, capacity, length
  • 字符串缓冲区(堆: 动态存储持续性): 由程序员自行管理内存, 但是存在析构函数在string对象本身被自动回收时, 同时回收缓冲区. (析构函数: 将堆内存与栈中string对象的生命周期绑定, 实现自动管理)

不安全的情况: 虽然通过析构函数可以保证对象本身的指针是安全的, 但是C++给予用户权利, 可以创建另一个指向该缓冲区的指针P, 当内存释放时, 如果继续使用P就会导致悬空指针.

875

Rust内存管理

Rust引入了所有权概念:

  1. 拥有者: 每个值都有决定其生命周期的唯一拥有者 (所有权代替了析构函数, 并成为值和拥有者的强制关系, 同时还有move等更多灵活的操作)
  2. drop: 当拥有者被释放时, 其所拥有的值也会被同时释放
  3. 变量(自动存储管理)拥有自己的值(强制绑定生命周期), 当控制流离开声明变量的块时, 变量丢弃, 其拥有的值也一并丢弃.
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
Pastedimage20250202150952.png

所有权树


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机制是足够简洁的.

Pastedimage20250202152951.png

Rust的扩展特性

扩展特性保证了所有权具有一定的灵活性:

  1. 可以将值从一个拥有者move到另一个拥有者. (允许用户构建, 重构, 拆除树结构)
  2. 整数, 浮点数, 字符等简单类型, 不受所有权规则约束. (Copy类型)
  3. 标准库提供引用计数指针类型Rc和Arc, 它们允许值在某些限制下有多个拥有者
  4. borrow值的引用, 这种引用是非拥有型指针, 有受限的生命周期.

4.2 移动

Rust中大多数类型的赋值操作实际上: 将值的所有者move给目标变量, 源变量回到为初始化状态, 改由目标变量来控制值的生命周期.

4.2.0.a Python的赋值操作

python使用引用计数来跟踪引用值的数量 (如下图所示)

开销问题: python需要垃圾回收机制, 来自动回收引用计数为0的值

s = ['udon', 'ramen', 'soba']
t = s
u = s
875

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;
Pastedimage20250202155721.png

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初始化后的内存图

Pastedimage20250202165517.png

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

Pastedimage20250202165612.png

尝试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

  1. 函数传参时会将所有权move给参数
  2. 函数返回时会将所有权move给调用者
  3. 构建元组会将所有权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限制

  1. 如果循环中不给源变量赋值, 无法move
  2. 如果循环中给源变量赋值, 可以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

  1. for循环move了v, v变为未初始化状态
  2. for循环机制获取向量所有权, 并将其分解为元素
  3. 所以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
}

内存视图

Pastedimage20250202185342.png

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();
875

循环引用问题

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

875

TODO

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