从C到Rust:堆内存,数据如何逃逸栈的约束?

堆内存 ------ 数据如何逃逸栈的约束

一、栈的约束:引用只能"向下"传递

1.1 栈是如何工作的

所有程序都使用栈(stack)管理函数调用。每次调用分配一个栈帧,函数返回时释放:

scss 复制代码
调用栈(从高地址向低地址增长):
┌──────────────────┐ ← 高地址
│ main()           │
│ ├─ 局部变量 x      │
├──────────────────┤
│ func_a() │
│ ├─ 局部变量 a      │
│ ├─ 参数 p         │ ← p 指向 main 中的 x(向下传递)
├──────────────────┤
│ func_b()         │
│ ├─ 局部变量 b      │
├──────────────────┤
│ func_c()         │
│ ├─ 局部变量 c      │
└──────────────────┘ ← 低地址(栈顶)

每个栈帧在函数返回时销毁。你不能持有一个指向已销毁栈帧的指针。

1.2 引用的"向下传递"

一个引用可以从调用者传递到被调用者------这是安全的,因为被调用者的生命周期包含在调用者的生命周期之内:

rust 复制代码
fn callee(x: &i32) { // x 来自上层(调用者)
println!("{}", x);
}

fn caller() {
let val = 42; // val 在 caller 的栈帧上
callee(&val); // 将 &val 向下传给了 callee
// callee 返回后,val 仍在 caller 中,仍然有效
}

引用从调用者流向被调用者,这是"向下"传递的意思。

1.3 但是引用不能"向上"传递

一个函数不能返回对自身局部变量的引用:

rust 复制代码
fn bad() -> &i32 {
let x = 42; // x 在 bad() 的栈帧上
&x // ❌ 编译错误:返回对局部变量的引用
} // bad() 返回后栈帧销毁,x 消失

// error: returns a reference to a local variable `x`

这正是栈的约束:数据死在它的栈帧里,你不能把它带出那个栈帧。

1.4 C 中同样的问题

C 编译器对此发出警告,但这不是错误:

c 复制代码
int* bad() {
int x = 42;
return &x; // ⚠️ 编译器警告,但代码仍然编译
} // x 被销毁,返回的指针成为悬垂指针

void caller() {
int* p = bad();
printf("%d", *p); // ❌ 未定义行为(可能恰好打印 42,也可能崩溃)
}

C 与 Rust 的差异:

维度 C Rust
返回局部变量地址 警告(可被忽略) 编译错误(不可绕过)
悬垂指针的防范 生命周期系统保证
替代方案 返回静态变量、malloc、或由调用者传入缓冲区 返回栈上的值本身、或堆分配

1.5 "向下传递"的严格形式

引用只能向下传递的严格形式是生命周期约束:

rust 复制代码
fn example<'a>(x: &'a i32) -> &'a i32 {
// ↑ ↑
// 返回的引用的生命周期 = 传入引用的生命周期
// 这意味着:返回的引用不会超过传入引用的存活时间
// 编译器在调用点检查这一约束
x
}

生命周期标注 'a 是编译器追踪引用"能活多久"的方式。 它确保引用永远不会向上逃逸出它的数据所在的作用域。

1.6 引用的本质:值的别名

引用(&T / &mut T)是 Rust 中访问数据的主要方式。理解引用和值的关系,是理解堆内存的关键。

引用是什么?

  • &T:对类型 T 的值的不可变引用------你可以读,但不能写

  • &mut T:对类型 T 的值的可变引用------你可以读,也可以写

rust 复制代码
let x = 42;
let r: &i32 = &x; // r 是 x 的不可变引用
// *r = 43; // ❌ 不能通过 &T 写入

let mut y = 42;
let r: &mut i32 = &mut y; // r 是 y 的可变引用
*r = 43; // ✅ 可以通过 &mut T 写入

引用不拥有值,它只是借用。这意味着引用离开作用域时不会释放值的内存------释放是所有者的事。

引用本身也是一个值。

引用占用内存(一个机器字),有地址,可以 Copy(&TCopy):

rust 复制代码
let x = 42;
let r1 = &x; // 对 x 的引用
let r2 = r1; // 复制引用(&T 是 Copy 的)
// r1 和 r2 都指向 x,两者都有效

1.7 值 vs 引用------两组正交的概念

理解 Rust 的关键:值有值的属性,引用有引用的属性,两者是正交的。

第一轴------值的可变性(在声明时决定):

rust 复制代码
let x = 42; // x 的值不可变
let mut y = 42; // y 的值可变

第二轴------引用的类型(在借用时决定):

rust 复制代码
let r1 = &x; // 不可变引用 ------ 只能读
let r2 = &mut y; // 可变引用 ------ 可读可写

正交表:

scss 复制代码
数据不可变(let x) 数据可变(let mut y)
& (不可变引用) ✅ ✅
&mut (可变引用) ❌ ✅

每个格子的解读:

ini 复制代码
第一格:let x = 42; let r = &x;
→ 数据不可变,引用也不可变 → 只能读
→ ✅ 安全共享

第二格:let mut y = 42; let r = &y;
→ 数据可变,但引用不可变 → 只能读
→ ✅ 安全:借出只读访问,所有权仍可写

第三格:let x = 42; let r = &mut x;
→ ❌ 编译错误!数据本身不可变时,不能借出可变引用
→ 因为 &mut 意味着"我要修改数据",但数据声明为不可变

第四格:let mut y = 42; let r = &mut y;
→ 数据可变,引用也可变 → 可读可写
→ ✅ 唯一写入路径

核心规则:&mut 要求数据本身是可变的。

这条规则的含义:

  • let x = 42 之后,不能写 let r = &mut x------编译器拒绝
  • let mut x = 42 之后,&mut x&x 都可以
  • 可变性在数据声明时就决定了,引用只能"映射"这一属性

1.8 Box/Rc/Arc 如何产生引用

堆包装类型通过 Deref trait 自动产生对被包装数据的引用。

Deref trait 的定义:

rust 复制代码
pub trait Deref {
type Target: ?Sized;
fn deref(&self) -> &Self::Target;
}

当你对 Box<T>Rc<T>Arc<T> 使用 *. 操作符时,编译器自动调用 deref()

rust 复制代码
let b = Box::new(42);
  
// 显式解引用:
let r: &i32 = &*b; // *b 通过 Deref 得到 i32,再取引用
let val: i32 = *b; // *b 得到 i32 的值(i32 是 Copy)

// 隐式解引用(方法调用时自动发生):
// . 操作符会自动尝试 deref 直到找到匹配的方法
println!("{}", b); // 自动 &b → &Box<i32> → deref → &i32 → Display

这就是"智能指针"的含义:Box / Rc / Arc 像一个指针一样工作------你通过它们访问堆上的数据,语法和直接使用引用一样。

scss 复制代码
栈上: 堆上:
┌──────────┐ ┌──────┐
│ Box<i32> │ deref()│ i32 │
│ ptr──────────────▶│ 42  │
└──────────┘ └──────┘

你的代码: b + 1
编译器理解: (*b) + 1
等价于: *(b.deref()) + 1
最终: 42 + 1

Rc<T>Arc<T> 的 Deref 实现完全一样 ------它们只是管理引用计数的方式不同,但最终都通过 deref() 返回 &T

rust 复制代码
let r = Rc::new(42);
let a = Arc::new(42);

println!("{}", r); // ✅ 通过 Deref 变成 &i32
println!("{}", a); // ✅ 同上

总结引用与堆包装类型的关系:

类型 拥有数据 Deref 目标 通过 Deref 得到
Box<T> 唯一 T &T
Rc<T> 共享(引用计数) T &T
Arc<T> 共享(原子引用计数) T &T
&T 不拥有(借用) --- 本身就是引用
&mut T 不拥有(借用) --- 本身就是引用

1.9 引用的"向下传递"与堆的关系

综合以上:引用是访问堆数据的通道,但引用本身是栈上的值。

rust 复制代码
let b = Box::new(42); // b(栈)→ 堆上的 42
let r: &i32 = &*b; // r(栈)→ 同一块堆内存

// 引用可以向下传递:
fn read(r: &i32) {
println!("{}", r);
}
read(r); // ✅ 引用向下传给 read

// 但引用不能向上传递(1.4 节的规则同样适用):
fn try_escape() -> &i32 {
let b = Box::new(42);
&*b // ❌ 编译错误!b 在函数返回时销毁
}

// 即使数据在堆上,引用本身受栈约束------Box 是局部变量

这里有一个微妙但重要的点:堆数据本身可以逃逸栈(返回 Box<T>),但引用不能------因为你返回引用时,引用指向的"东西"必须还在。 返回 Box<T> 是把拥有权转给调用者,调用者接收后堆数据继续存活。返回 &T 只是"借",借的东西得有人一直持有。

rust 复制代码
// ✅ 正确:把拥有权转给调用者
fn make() -> Box<i32> {
let b = Box::new(42);
b // 所有权随 Box 转移
}

// ❌ 错误:引用无法逃逸
fn borrow() -> &i32 {
let b = Box::new(42);
&*b // b 被 Drop 后,引用悬空
}

// ✅ 正确:引用从传入的参数来
fn reborrow<'a>(b: &'a Box<i32>) -> &'a i32 {
&*b // 返回的引用寿命跟传入参数一样长
}

1.10 那如果需要"向上"传递怎么办?

有三种方式可以将数据从被调用者传递给调用者:

rust 复制代码
// 方式 1:返回值本身(栈上传递)

fn make_value() -> i32 {

42 // ✅ 返回栈上的值

}

  


// 方式 2:由调用者传入写目标

fn fill(buf: &mut [i32]) {

buf[0] = 42; // ✅ 写入调用者提供的缓冲区

}

  


// 方式 3:堆分配(本章的主题)

fn make_on_heap() -> Box<i32> {

Box::new(42) // ✅ 在堆上分配,返回所有权

}

方式 1 和 2 仍然受栈约束------数据最终还在调用者的栈上。方式 3 是唯一真正"逃逸"栈的方式:数据在堆上,它的生命周期不受创建它的函数控制。


二、堆:跳出栈约束的唯一方式

2.1 堆的本质

堆(heap)是一块独立于调用栈的内存区域:

复制代码
内存布局:

┌────────────────────┐
│ 代码段 │ 程序的指令
├────────────────────┤
│ 栈 │ 每次函数调用分配,返回时释放(LIFO)
├────────────────────┤
│ 堆 │ 手动请求分配,手动或自动释放
├────────────────────┤
│ 数据段 │ 全局变量、静态变量
├────────────────────┤
│ 常量区 │ 字符串常量等
└────────────────────┘

栈和堆的根本区别:

维度
分配方式 自动(函数调用时) 手动请求(malloc / Box::new
释放时机 函数返回时 手动 free 或自动 Drop
生命周期 绑定于栈帧 独立于栈帧
速度 极快(一条指令) 较慢(分配器查找空闲块)
大小限制 小(通常几 MB) 大(取决于物理内存)

2.2 堆解决了"栈的约束"

当数据在堆上时,它的生命周期不受创建它的栈帧约束:

rust 复制代码
fn make_string() -> Box<String> {

let s = Box::new(String::from("hello"));

// s 在堆上,Box 是栈上的指针

s // 返回 Box(栈上的指针),堆上的 String 仍然存活

} // 调用者的栈帧接收 Box,堆上的 String 继续存在

  


fn caller() {

let s = make_string(); // s 是 Box<String>

println!("{}", s); // ✅ 堆上的字符串仍然有效

} // 当 s 离开 caller 的作用域时,

// Box 的 Drop 释放堆上的 String

堆数据可以安全地在函数间传递,因为:栈上只传递指针,数据本身在堆上不动。谁拥有指针,谁就拥有数据。

2.3 C 中的堆:malloc/free------原始、直接、危险

c 复制代码
#include <stdlib.h>

  


char* make_string() {

char* s = malloc(100);

if (!s) return NULL;

strcpy(s, "hello");

return s; // 返回堆内存,调用者必须 free

}

  


void caller() {

char* s = make_string();

printf("%s", s);

free(s); // 必须记得------否则内存泄漏

}

C 中堆分配的五个问题:

c 复制代码
// 1. 可能返回 NULL ------ 必须检查

char* p = malloc(100);

if (!p) return -1;

  


// 2. 必须记得手动 free ------ 容易漏掉

// (漏了就是内存泄漏)

  


// 3. free 后还能用 ------ use-after-free

free(p);

p[0] = 'a'; // ❌ 未定义行为(编译器不说 NO)

  


// 4. 可能 double-free

free(p);

free(p); // ❌ 未定义行为

  


// 5. 不知道 malloc 的是谁、该谁 free

// char* process_data(const char* input);

// 返回的指针是 malloc 过的吗?需要 free 吗?

// 只能看文档,文档可能不准确

小结:

rust 复制代码
C 的堆分配: Rust 的堆分配:

malloc → free Box::new → Drop::drop

↑ 手动配对 ↑ 自动配对

↑ 易错 ↑ 编译器保证

2.4 Rust 的堆分配哲学

Rust 把堆内存的分配和释放封装在包装类型中,通过所有权和 Drop 确保安全:

rust 复制代码
// 所有堆内存操作都通过类型完成

let b = Box::new(42); // 堆上分配一个 i32

let r = Rc::new(42); // 堆上分配一个引用计数的 i32

let a = Arc::new(42); // 堆上分配一个原子引用计数的 i32

  


// 释放时------自动

// b、r、a 离开作用域时,各自的 Drop 自动释放堆内存

三种类型的核心理念:

类型 所有权模式 线程安全 开销 C 的对应
Box<T> 唯一所有权 Send(如果 T: Send) 无额外开销 malloc + free
Rc<T> 共享所有权(引用计数) 非 Send(单线程) 非原子计数 手动 refcount
Arc<T> 共享所有权(原子引用计数) Send + Sync 原子计数 __sync_fetch_and_add

以下三节逐一详解。


三、Box<T> ------ 唯一所有权的堆分配

3.1 C 的对应:malloc + free

c 复制代码
int* p = (int*)malloc(sizeof(int));

if (p == NULL) return -1; // 必须检查 NULL

*p = 42;

  


// 使用 p ...

  


free(p); // 必须记得释放

// p 仍然可以访问(悬垂指针)

C++ 中的等效:std::unique_ptr<int>(new int(42))------同样也是唯一所有权、自动释放。

3.2 Rust 的 Box

rust 复制代码
let b = Box::new(42); // 堆分配,值初始化为 42

// 使用 b ...

println!("{}", b); // 自动解引用:显示 42

// b 离开作用域 → Drop 自动释放堆内存

Box<T> 是一个拥有所有权的指针:

rust 复制代码
栈上: 堆上:

┌──────────┐ ┌──────┐
│ Box<i32> │ ─────▶ │ i32 │
│ ptr      │ │ 42   │
└──────────┘ └──────┘

3.3 Box 解决了 C 的哪些问题

rust 复制代码
// 问题 1:Box 不可能为 NULL

let b = Box::new(42);

// let b: Box<i32> = ??? // 不可能为空

  


// 问题 2:离开作用域自动 Drop------不会忘记 free

{

let b = Box::new(42);

} // ✅ 自动释放

  


// 问题 3:所有权唯一------不会 double-free

let b1 = Box::new(42);

let b2 = b1; // move:所有权给 b2

// b1 已失效

// b2 离开时 Drop 只调用一次 ✅

  


// 问题 4:use-after-free 不可能

let b = Box::new(42);

std::mem::drop(b); // 显式释放

// println!("{}", b); // ❌ 编译错误:b 已移出

3.4 Box<T> 的典型使用场景

场景 1:将大对象移到堆上

rust 复制代码
// 栈上的大数组

let large_array: [u8; 1024 * 1024] = [0; 1024 * 1024]; // 1MB 栈空间!

// 可能导致栈溢出

  


// 堆上的大数组

let large_box: Box<[u8; 1024 * 1024]> = Box::new([0; 1024 * 1024]); // 堆上

// 栈上只有指针,安全

场景 2:递归类型

rust 复制代码
// Rust 中递归类型必须使用 Box

enum List<T> {

Cons(T, Box<List<T>>), // Box 打破循环------大小固定

Nil,

}

// 如果没有 Box,编译器不知道 List 有多大(无限递归)

场景 3:Trait 对象(动态分发)

rust 复制代码
// 不同大小的类型通过 Box 统一

trait Animal {

fn speak(&self);

}

  


struct Dog;

impl Animal for Dog {

fn speak(&self) { println!("woof"); }

}

  


struct Cat;

impl Animal for Cat {

fn speak(&self) { println!("meow"); }

}

  


let animals: Vec<Box<dyn Animal>> = vec![

Box::new(Dog),

Box::new(Cat),

]; // ✅ 不同的类型通过 Box<dyn Animal> 统一存储

场景 4:转移堆数据的所有权

rust 复制代码
fn process(data: Box<Vec<i32>>) {

// 处理 data ...

} // data 在这里自动释放

  


fn main() {

let v = Box::new(vec![1, 2, 3]);

process(v); // ✅ 所有权转移给 process

// v 已失效

}

3.5 零成本抽象:Box 编译后就是 malloc/free

rust 复制代码
let b = Box::new(42);

// 编译为 ≈

// int* b = malloc(sizeof(int));

// *b = 42;

// (如果 malloc 失败,程序 panic)

  


drop(b);

// 编译为 ≈

// free(b);

Box<T> 没有添加任何额外的检查或元数据。它的开销就是 malloc / free 的开销------和 C 完全一致。多出来的只是编译期的安全检查。

3.6 C vs Rust 对比

维度 C (malloc + free) Rust (Box<T>)
分配失败 返回 NULL(必须检查) panic(不可忽略)
释放时机 手动 free() 自动 Drop
所有权 无(多个指针可指向同一块内存) 唯一(赋值 = move)
空悬指针 可能(free 后仍可解引用) 不可能(移出后编译器拒绝访问)
双重释放 可能 不可能
类型安全 void* 需手动 cast 完整类型信息
运行时开销 与 Box 相同(都是 malloc/free) 与 C 相同

四、Rc<T> ------ 单线程引用计数共享所有权

4.1 C 中的对应:手动引用计数

当多个部分需要共享同一块数据时,唯一所有权(Box)不够用。C 中常用引用计数:

c 复制代码
struct RefCounted {

int refcount;

char* data;

// ...

};

  


void ref_inc(struct RefCounted* obj) {

obj->refcount++;

}

  


void ref_dec(struct RefCounted* obj) {

obj->refcount--;

if (obj->refcount == 0) {

free(obj->data);

free(obj);

}

}

  


// 使用时:

struct RefCounted* p = create_object();

ref_inc(p); // 新用户加入

// ... 使用 ...

ref_dec(p); // 用户离开

ref_dec(p); // 原始用户离开 → 释放

C 中手动引用计数的问题:

c 复制代码
// 1. 忘记 ref_inc → 提前释放

// 2. 忘记 ref_dec → 内存泄漏

// 3. 线程安全问题:非原子操作下多线程同时 inc/dec → 计数错误

// 4. 循环引用:A 引用 B,B 引用 A → 两者引用计数永不为 0

C++ 中的等效:std::shared_ptr<T>------与 Rc 类似,但 std::shared_ptr 是线程安全的(原子计数),因此更像 Arc

4.2 Rust 的 Rc<T>

rust 复制代码
use std::rc::Rc;

  


let original: Rc<i32> = Rc::new(42);

  


let shared1 = Rc::clone(&original); // 或 original.clone()

let shared2 = Rc::clone(&original); // 或 original.clone()

  


println!("{}", shared1); // ✅ 三个 Rc 共享同一个 i32

println!("{}", shared2); // ✅

// 引用计数 = 3

  


// 当 original、shared1、shared2 离开作用域时

// 引用计数递减,最后一次 Drop 释放堆内存

4.3 Rc::clone vs Box 的 clone

关键区别:

rust 复制代码
let b = Box::new(42);

let b2 = b.clone(); // ✅ 深拷贝:新分配堆内存,复制值

  


let r = Rc::new(42);

let r2 = Rc::clone(&r); // ✅ 浅拷贝:只递增引用计数,不复制值

// 两个 Rc 指向同一个堆内存

对于 Rcclone() 不是"复制数据",而是"增加了一个共享者"。这是 C 的手动引用计数中 ref_inc 做的事。

4.4 Rc 与 C 的引用计数对比

维度 C 手动 refcount Rust Rc<T>
递增 手动调用 ref_inc(obj) Rc::clone(&rc) 自动递增
递减 手动调用 ref_dec(obj) Drop 自动递减
释放条件 手动检查 refcount == 0 自动:计数到 0 时 Drop 释放
忘记递增 可能提前释放 不可能------clone 是唯一方式
忘记递减 内存泄漏 不可能------Drop 保证
循环引用 需要手动打破 Weak<T> 解决

4.5 Weak<T>:打破循环引用

当两个 Rc 互相引用时,它们的引用计数永远不会降为 0:

rust 复制代码
use std::rc::Rc;

  


// 这种情况会导致内存泄漏:

{

let a = Rc::new(42);

let b = Rc::clone(&a); // 只是普通的共享

} // ✅ 引用计数 2→1→0,正确释放

  


// 但如果是互相持有:

struct Node {

value: i32,

next: Option<Rc<Node>>,

}

  


// 如果 Node A.next = Node B, Node B.next = Node A

// A 的 refcount = 1(B 持有),B 的 refcount = 1(A 持有)

// 两者都不会释放 → 内存泄漏

Weak<T> 解决这个问题:

rust 复制代码
use std::rc::{Rc, Weak};

  


struct Node {

value: i32,

parent: RefCell<Weak<Node>>, // parent 用 Weak------不增加引用计数

children: RefCell<Vec<Rc<Node>>>,

}

  


// 当父节点释放后,子节点通过 Weak::upgrade() 获取父节点引用

// 返回 Option<Rc<T>>------如果父节点已释放,返回 None

Weak<T> 的意义:"我想引用你,但我不决定你的生命周期。"

4.6 适用场景

rust 复制代码
// 1. 图的共享节点

// 2. 树的子节点需要访问父节点(父用 Weak)

// 3. 缓存:多个消费者共享同一数据

// 4. 不可变数据的多方共享

  


// 注意:Rc<T> 中的数据是不可变的!

// 如果需要可变性,需要 Rc<RefCell<T>>(但在单独章节讨论)

五、Arc<T> ------ 多线程引用计数共享所有权

5.1 C 中的对应:原子引用计数

多线程环境下,普通的 refcount++ 不是线程安全的------两个线程可能同时读到旧值,都加 1,结果计数只增加了 1(而不是 2):

c 复制代码
// 错误的做法(非原子操作):

void ref_inc(struct RefCounted* obj) {

obj->refcount++; // ❌ 可能被两个线程同时执行

}

  


// 正确的做法(使用原子操作):

#include <stdatomic.h>

  


void ref_inc_atomic(struct RefCounted* obj) {

atomic_fetch_add(&obj->refcount, 1);

// 或 __sync_fetch_and_add(&obj->refcount, 1)

}

C 中原子引用计数的问题:

  • 需要正确的内存序(memory order)------ memory_order_relaxed vs acquire vs release

  • 容易选错顺序 → 难以调试的并发 bug

  • C 标准在 C11 之前没有原子操作(只能用编译器扩展)

5.2 Rust 的 Arc<T>

rust 复制代码
use std::sync::Arc;

use std::thread;

  


let shared: Arc<i32> = Arc::new(42);

  


let mut handles = vec![];

  


for _ in 0..10 {

let arc_clone = Arc::clone(&shared);

let handle = thread::spawn(move || {

println!("{}", arc_clone); // ✅ 多线程共享

});

handles.push(handle);

}

// 引用计数 = 1(原始) + 10(线程中的 clone) = 11

  


for handle in handles {

handle.join().unwrap();

}

// 线程结束后,引用计数递减 → 最后降为 0 → Drop 释放

ArcRc 的唯一区别:Arc 使用原子操作管理引用计数,Rc 使用非原子操作。

rust 复制代码
// Rc:高效,但不是线程安全的

// 不能跨线程传递(!Send)

let rc = Rc::new(42);

// thread::spawn(move || { println!("{}", rc); }); // ❌ Rc 不是 Send

  


// Arc:有原子操作的开销,但线程安全

let arc = Arc::new(42);

thread::spawn(move || { println!("{}", arc); }); // ✅ Arc 是 Send

5.3 Rc vs Arc 的性能差异

rust 复制代码
// Rc::clone() 的开销 = 一次非原子加法

// 在现代 CPU 上:~1 纳秒

  


// Arc::clone() 的开销 = 一次原子加法(含内存屏障)

// 在现代 CPU 上:~10-20 纳秒

  


// 在单线程场景下,总是用 Rc

// 在多线程场景下,只能用 Arc

5.4 C vs Rust 对比

维度 C 原子引用计数 Rust Arc<T>
操作方式 手动 atomic_fetch_add + 判断 Arc::clone() + 自动 Drop
内存序 程序员手动选择(易错) 自动选择正确顺序
线程安全 取决于是否正确使用原子操作 强制:Arc 实现了 Send + Sync
循环引用 同 Rc 的问题 Weak<T> 解决
开销 原子操作 同样开销

六、补充主题

6.1 栈 vs 堆:性能与选用原则

选用顺序(从优到劣):

php 复制代码
1. 优先栈分配

let x = T { ... }; // 最快,一条指令

  


2. 如果需要堆 + 唯一所有权

Box::new(val) // 零开销包装

  


3. 如果需要堆 + 单线程共享

Rc::new(val) // 引用计数(非原子)

  


4. 如果需要堆 + 多线程共享

Arc::new(val) // 引用计数(原子)

性能对比:

scss 复制代码
栈分配:mov rsp, [rsp - size] → ~1 周期
堆分配:malloc() → ~50-100 周期(取决于分配器)
堆释放:free() → ~50-100 周期

Box/Rc/Arc 的分配和释放与 C 的 malloc/free 成本相当。Rust 没有 GC,所以也没有 GC 暂停的问题。

6.2 裸指针不是堆包装

Rust 中有与 C 指针一一对应的裸指针,但它们不是管理堆内存的工具:

rust 复制代码
// *const T = C 的 const T*

// *mut T = C 的 T*

  


let mut x = 42;

let ptr: *mut i32 = &mut x; // 裸指针可以指向栈

let raw: *const i32 = &x; // 也可以指向堆

  


// 但裸指针:

// ❌ 不拥有内存

// ❌ 解引用需要 unsafe

// ✅ 主要用于 FFI 与 C 交互

  


// 日常堆管理中不要使用裸指针

// 应使用 Box / Rc / Arc

6.3 堆包装类型的零成本抽象

Rust 的堆包装类型编译后与 C 代码等价:

rust 复制代码
// Rust 源码

let b = Box::new(42);

println!("{}", b);

drop(b);
c 复制代码
// 编译后 ≈ C 代码

int* b = malloc(sizeof(int));

if (b == NULL) panic(); // OOM 处理

*b = 42;

printf("%d\n", *b);

free(b);

Rust 在这里没有添加任何额外的运行时。零成本抽象的意思是:你得到的所有"安全检查"都在编译期完成,运行时与手写 C 代码相同。

6.4 自定义分配器

C 中可以通过 LD_PRELOAD 替换 malloc/free

Rust 中可以通过 #[global_allocator] 替换全局分配器:

rust 复制代码
use std::alloc::{GlobalAlloc, Layout};

  


struct MyAllocator;

  


unsafe impl GlobalAlloc for MyAllocator {

unsafe fn alloc(&self, layout: Layout) -> *mut u8 {

// 自定义分配逻辑

}

unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {

// 自定义释放逻辑

}

}

  


#[global_allocator]

static ALLOCATOR: MyAllocator = MyAllocator;

  


// 之后所有的 Box::new / Rc::new / Arc::new 都会使用 MyAllocator

这与 C 的 LD_PRELOAD 不同------Rust 的分配器替换是类型安全的,只在 Rust 代码中有效,而 LD_PRELOAD 会拦截所有 malloc 调用(包括 C 库内部的)。

6.5 三种类型的完整对比表

维度 Box<T> Rc<T> Arc<T>
所有权 唯一 共享(引用计数) 共享(原子引用计数)
线程安全 Send(如果 T: Send) !Send(单线程) Send + Sync
clone 行为 深拷贝数据 递增引用计数 递增原子引用计数
drop 行为 释放堆内存 递减引用计数,到 0 释放 递减原子引用计数,到 0 释放
额外开销 一个非原子计数器 一个原子计数器
循环引用 不可能(唯一所有权) 可能(用 Weak 解决) 可能(用 Weak 解决)
数据可变性 通过 &mut T 仅共享引用(不可变) 仅共享引用(不可变)
C 的直接对应 malloc + free 手动非原子引用计数 手动原子引用计数
C++ 相似 std::unique_ptr --- std::shared_ptr

七、与 C 程序员的对话

对话一:"Box 不就是 malloc 包装了一下?"

C 程序员 :"Box::new 就是调用了 malloc 吧?那和直接 malloc 有什么区别?"
Rust :"运行时的确调用了 malloc,区别不在运行时在编译期。Box 让编译器知道你拥有这块堆内存------因此它会自动释放,不会 double-free,不会 use-after-free。这些 C 中要花几周调试的问题,Rust 在编译期就排除了。"

c 复制代码
// C ------ 编译通过,运行崩溃

int* p = malloc(sizeof(int));

free(p);

free(p); // ❌ 编译通过,运行崩溃
rust 复制代码
// Rust ------ 编译不通过

let p = Box::new(42);

drop(p);

drop(p); // ❌ 编译错误:p 已被移出

对话二:"Rc 就是手动引用计数的自动化"

C 程序员 :"我在 C 中用过引用计数,就是 refcount++ / refcount--。Rc 不就是把它自动化了吗?"
Rust:"是的,但自动化就是全部的区别。手动引用计数中,你每一次 clone(refcount++)和 drop(refcount--)都需要你亲自写。在大型项目中,你总会漏掉一个------要么忘记 ++ 导致过早释放,要么忘记 -- 导致泄漏。Rust 让编译器替你管理这些配对操作。"

c 复制代码
// C ------ 手动 refcount,漏了一个 ++ 或 -- 就是 bug

struct User* u = create_user();

ref_inc(u); // 传给另一个模块

ref_inc(u); // 自己再保留一份

process(u);

ref_dec(u); // 自己用完

// 但另一个模块中的 ref_dec 呢?如果忘记 ------ 泄漏
rust 复制代码
// Rust ------ clone 自动 ++,drop 自动 --

let u = Rc::new(User::new());

let u2 = Rc::clone(&u); // refcount++

let u3 = Rc::clone(&u); // refcount++

// u2、u3、u 各自自动 Drop → refcount 递减到 0 → 释放

对话三:"那 Arc 不就是加了原子操作的 Rc?"

C 程序员:"理解了。Rc 是非原子引用计数(单线程用),Arc 是原子引用计数(多线程用)。那和 C 中的原子操作比有什么优势?"
Rust :"和 C 的原子引用计数一样的性能开销。但 C 中你需要自己选择正确的内存序------relaxedacquirereleaseacq_rel。选错了就是诡异的并发 bug。Rust 的标准库内部已经为你选好了,你不用操心。"

c 复制代码
// C ------ 内存序选错可能导致 bug

atomic_fetch_add(&refcount, 1, memory_order_relaxed);

// ↑ 这个 relaxed 可能合适,但也可能不

// 如果错了,调试极其困难
rust 复制代码
// Rust ------ 内部使用正确顺序

let a = Arc::new(data);

let a2 = Arc::clone(&a); // ✅ 内部使用正确内存序

八、小结

8.1 核心逻辑链

swift 复制代码
引用只能"向下"传递(调用者 → 被调用者)

↓

不能返回对局部变量的引用(栈帧约束)

↓

要跳出栈的约束 → 堆内存

↓

堆内存通过包装类型管理:

Box<T> = 唯一所有权 ≈ malloc + free

Rc<T> = 共享(单线程) ≈ 手动 refcount

Arc<T> = 共享(多线程) ≈ 原子 refcount

8.2 解决的核心 C 问题

C 的问题 如何解决
malloc / free 手动配对 Box / Rc / ArcDrop 自动释放
返回局部变量地址 堆分配 + 返回 Box<T>
谁该 free 不确定 从返回类型直接看出:Box = 唯一拥有者
忘记 free 无法忘记------离开作用域自动触发
double-free 所有权唯一 + move 后原值失效
use-after-free 移出后编译器禁止访问
手动 refcount 配对 Rc::clone / Drop 自动配对
原子 refcount 内存序 Arc 内部使用正确顺序

8.3 C 程序员的三句话

Box<T> :就是 malloc + free,但自动配对,无法忘记。
Rc<T> :就是手动引用计数,但 clone 自动 ++、drop 自动 --,不会漏。
Arc<T>:就是原子引用计数,但内存序自动选好,不用你操心。
堆的整体:当数据需要逃逸栈帧时,堆是唯一路径。Rust 的堆包装类型让这条路径是安全的。

相关推荐
doiito(Do It Together)2 小时前
【安全,架构】RustyVault 项目深度分析报告以及和HashiCorp Vault的差异
微服务·架构·rust
问窗3 小时前
Rust实现Windows本地搜索工具
windows·搜索引擎·rust
程序员爱钓鱼4 小时前
Rust 变量与不可变性:为什么默认不能修改变量?
前端·后端·rust
第一程序员15 小时前
Rust Agent 子进程执行:Command 之前,先定义输入和超时
python·rust·github
IT笔记16 小时前
【Rust】Rust Match 模式匹配详解
java·开发语言·rust
取地址符21 小时前
Rust学习笔记(基于Rustlings)(2):数据结构与集合
笔记·学习·rust
取地址符1 天前
Rust语法学习 (基于Rustlings)(1):基础语法
经验分享·笔记·学习·rust
记忆着1 天前
Rust基础入门二
rust