C 里动态分配通常从这几个函数开始:
void *p = malloc(size);
p = realloc(p, new_size);
free(p);
Rust 普通代码很少直接调用分配器。更常见的是用拥有类型表达“这块内存归谁管”:
Box<T> -> 堆上的一个 T
Vec<T> -> 堆上的连续可增长数组
String -> 堆上的可增长 UTF-8 文本
这些类型内部当然会分配和释放内存,但调用方通常不写 free。值离开作用域时,释放由类型的 Drop 完成。
Box 对应堆上的一个对象
C 里为一个结构体分配内存:
struct Config *cfg = malloc(sizeof *cfg);
if (!cfg) {
return -1;
}
cfg->port = 1883;
cfg->retry = 3;
free(cfg);
Rust 里如果确实需要把一个值放到堆上,可以用 Box<T>:
struct Config {
port: u16,
retry: u8,
}
let cfg = Box::new(Config {
port: 1883,
retry: 3,
});
Box<Config> 表示:
拥有一个堆上的 Config
cfg 离开作用域时,Config 和那块堆内存都会释放。通常不用手写释放函数。
不是所有东西都要 Box
C 程序员容易把“需要一个对象”直接联想到 malloc。Rust 里很多对象直接放在栈上就够了:
let cfg = Config {
port: 1883,
retry: 3,
};
需要 Box<T> 的常见原因不是“创建对象”,而是:
- 对象很大,不想放在栈上
- 类型大小在编译期不好直接表达,例如递归数据结构
- 需要稳定的堆地址
- trait object 场景,例如
Box<dyn Writer> - 要把所有权作为一个堆对象传递
递归结构的经典例子:
enum List {
Cons(i32, Box<List>),
Nil,
}
如果没有 Box,List 会包含另一个完整的 List,大小无限展开。Box<List> 把下一段放到堆上,当前 enum 里只保存一个拥有指针。
Vec 对应可增长数组
C 里经常自己维护动态数组:
uint8_t *buf = malloc(capacity);
size_t len = 0;
if (len == capacity) {
capacity *= 2;
buf = realloc(buf, capacity);
}
buf[len++] = byte;
Rust 里通常用 Vec<T>:
let mut buf = Vec::new();
buf.push(0x10);
buf.push(0x20);
Vec<T> 可以先理解成:
ptr + len + capacity
查看长度和容量:
println!("len = {}", buf.len());
println!("capacity = {}", buf.capacity());
预留容量:
let mut buf = Vec::with_capacity(1024);
如果 push 时容量不够,Vec 会重新分配并移动数据。这和 C 的 realloc 很像。
reallocation 会让旧指针失效
C 里 realloc 后继续用旧指针是典型 bug:
uint8_t *old = buf;
buf = realloc(buf, new_capacity);
old[0] = 1; // 可能已经悬空
Rust 安全代码会尽量避免这种关系被随手写出来。但如果你拿了 Vec 的裸指针,再让 Vec 增长,就要自己负责:
let mut buf = Vec::with_capacity(1);
buf.push(1);
let ptr = buf.as_ptr();
buf.push(2); // 可能重新分配
// ptr 可能已经失效,不要继续解引用
普通 Rust API 里不要把 Vec 的内部指针长期暴露出去。跨 FFI 时尤其要小心:只要 C 保存了指针,Rust 侧就不能随便让 Vec 重新分配。
Vec 和 &[T] 的边界
函数只是读取一段数据,不应该接收 Vec<T>:
fn checksum(data: &[u8]) -> u32 {
data.iter().map(|b| *b as u32).sum()
}
函数要创建并返回动态数据,才返回 Vec<T>:
fn make_packet(payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(payload.len() + 2);
out.push(0xAA);
out.extend_from_slice(payload);
out.push(0x55);
out
}
这条边界很重要:
&[T] -> 借用一段现有数据
Vec<T> -> 拥有一段可增长数据
不要因为底层都是指针和长度,就把所有参数都写成 Vec<T>。
String 是 Vec 的文本版本
C 里动态字符串通常是:
char *s = malloc(capacity);
strcpy(s, "device");
free(s);
Rust 的 String 表示拥有的、可增长的 UTF-8 文本:
let mut name = String::from("device");
name.push_str("-01");
它和 Vec<u8> 很像:
Vec<u8> -> 拥有可增长字节
String -> 拥有可增长 UTF-8 文本
如果函数只是读取文本,接收 &str:
fn print_name(name: &str) {
println!("{name}");
}
如果函数创建并返回文本,返回 String:
fn make_topic(device: &str) -> String {
format!("devices/{device}/status")
}
String 不是 C 的 char *。它不保证 NUL 结尾,也不允许内部字节不是 UTF-8。C 字符串边界仍然用 CString / CStr。
容器释放由所有权决定
C 里释放通常靠约定:
uint8_t *make_packet(size_t *out_len);
void free_packet(uint8_t *p);
调用方必须记得调用匹配的释放函数。
Rust 里返回拥有类型:
fn make_packet() -> Vec<u8> {
vec![0xAA, 0x01, 0x55]
}
调用方拿到 Vec<u8>,也拿到了释放责任。但这个责任不是手写 free,而是值离开作用域时自动释放。
如果函数接收 Vec<u8>:
fn consume_packet(packet: Vec<u8>) {
println!("len = {}", packet.len());
}
调用方把所有权交给函数。函数结束时,packet 释放。
如果函数只借用:
fn inspect_packet(packet: &[u8]) {
println!("len = {}", packet.len());
}
调用方仍然拥有原来的 Vec。
什么时候需要 clone
C 里复制一段动态内存通常要 malloc + memcpy:
uint8_t *copy = malloc(len);
memcpy(copy, src, len);
Rust 里克隆拥有数据:
let a = vec![1_u8, 2, 3];
let b = a.clone();
clone() 对 Vec 和 String 会复制底层数据,分配新内存。它不是廉价的指针复制。
如果只是读取,不要为了绕过所有权问题就 clone:
fn print_len(data: &[u8]) {
println!("{}", data.len());
}
let data = vec![1_u8, 2, 3];
print_len(&data);
借用能解决的问题,不要用复制来掩盖。
这一层先记住什么
C 动态分配到 Rust 的第一层对应关系:
malloc one object -> Box<T>,但不是所有对象都需要 Box
malloc array / realloc -> Vec<T>
dynamic C string -> String,前提是 UTF-8 文本
pointer + len read -> &[T]
pointer + len write -> &mut [T]
malloc + memcpy copy -> clone,但要知道它会复制数据
free -> 拥有类型离开作用域时 Drop
Rust 不是没有堆分配,而是把堆分配藏在拥有类型里。Box、Vec、String 的重点不是“更方便的 malloc”,而是把谁拥有、谁能增长、什么时候释放写进类型。