跳转到主内容动态分配和容器 | 物联网民工

动态分配和容器

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”,而是把谁拥有、谁能增长、什么时候释放写进类型。

继续阅读