从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 的堆包装类型让这条路径是安全的。

相关推荐
传奇开心果编程10 小时前
【Xilem 0.4 基础语法学与练】第一课:从零到计数器
学习·rust·前端框架
Flynt17 小时前
pnpm 12 换上了 Rust 内核,我拿项目实测了一轮构建速度
rust·vite·前端工程化
传奇开心果编程21 小时前
【Xilem基础语法学与练】第8课:条件渲染(one_of)
学习·rust·前端框架
qwsaedca1 天前
在Mac上跑 Kokoro TTS经验总结
rust·mac·tts
qwsaedca1 天前
Kokoro TTS v1.1 voices 文件格式逆向分析
rust
老猿讲编程1 天前
【Eclipse OpenSOVD学习之五】拓扑引擎(Topology)
学习·rust·eclipse·sovd
object not found1 天前
Nuxt4去掉body中默认的边距
开发语言·后端·rust
梦醒沉醉2 天前
4、Rust参考手册——Crate和源文件
rust
chainbees2 天前
Windows 系统 Rust 运行环境搭建
rust