Rust Vec/String 内存布局与扩容
更新时间:2026-09-18。Vec 和 String 是 Rust 最常用的两个堆容器,理解它们的「(指针, 长度, 容量)」三件套能解释几乎所有相关性能现象。所有权基础见 所有权与借用规则。
一、Vec 的内存布局
Vec<T> 在栈上只是一个三元组,真正的数据在堆上连续存放:
bash
Vec { ptr: *mut T, len: usize, capacity: usize }
│
▼ 堆上连续的 capacity 个 T
[ T0 ][ T1 ][ T2 ] ... [ 预留空间 ... ]
└────── len=3 ──────┘
└──────── capacity=8(已分配,未初始化部分不可访问)────────┘len:当前有效元素数。capacity:堆上已分配、能容纳的元素数。- 连续布局 → 顺序遍历可被 CPU 预取,缓存友好;
v[i]是 O(1) 直接偏移。
二、扩容:什么时候 realloc
当 push 时 len == capacity,Vec 会申请更大内存(容量近似倍增,并按元素对齐做处理)、把旧元素 memcpy 过去、释放旧内存。频繁扩容 = 多次分配 + 拷贝,所以已知大小时预分配:
rust
let mut v: Vec<u32> = Vec::with_capacity(10_000); // 一次分配到位
(0..10_000).for_each(|i| v.push(i));
v.shrink_to_fit(); // 若之后元素很少,可收缩(会再分配)
assert!(v.len() <= v.capacity());reserve(extra) 可额外预留;clear() 只把 len 置 0(元素 drop)但保留容量,适合复用缓冲区。
三、String = Vec<u8> + UTF-8 约束
String 内部就是 Vec<u8>,同样有 len()(字节数)和 capacity(),外加「内容必须是合法 UTF-8」的不变量。&str 是对其中一段字节的胖指针(指针 + 字节长度)。
rust
let mut s = String::with_capacity(64);
s.push_str("hello");
s.push(','); // 一个 char 可能占多字节关键坑:len 是字节数不是字符数,中文/emoji 一个字符占 3~4 字节。
四、常见坑
- 按下标取字符:
s[0]不成立、&s[..1]在字符边界中间切割会 panic。遍历字符用.chars(),按字节切片要保证落在 UTF-8 边界(is_char_boundary)。 - 循环里 push 不预分配:触发多次倍增拷贝,用
with_capacity。 - 混淆
len()与capacity():判断「还能装多少」用capacity - len。 - String 拼接误区:
format!/push_str会扩容,循环拼字符串用一个String::with_capacity+push_str(可对照 Java StringBuilder)。 clear后以为内存还了:clear 保留容量,要真还内存用shrink_to_fit(有再分配成本,别滥用)。- 大量小 Vec/String:每个都一次堆分配,热点上考虑 arena/池化或内联小串优化库。
五、与其他容器的取舍
- 元素数固定或编译期已知:用数组
[T; N](栈上、无堆分配)。 - 需要在头部频繁增删:
VecDeque(环形缓冲),别用 Vec 的insert(0,..)(整体搬移 O(n))。 - 键值查找:
HashMap。连续 Vec 仍是遍历/随机访问的默认首选。
总结
Rust 的 Vec 在栈上只是指针加 len 加 capacity、数据在堆上连续存放,String 就是带 UTF-8 合法性约束的 Vec<u8>;当 len 等于 capacity 再 push 就会申请近似翻倍的新内存并 memcpy 旧元素,因此已知数量要用 with_capacity 一次到位、clear 保留容量可复用、shrink_to_fit 才真正还内存;务必记住 len/capacity 都是字节或元素个数、String 的 len 是字节数且按字节切片必须落在 UTF-8 字符边界否则 panic,连续布局带来缓存友好,固定大小用数组、头部增删用 VecDeque。