在 Rust 开发中,Vec 绝对是最常用的集合类型——小到存储接口返回的列表数据,大到处理百万级别的日志条目,都离不开它。但很多同学用 Vec 时只关注'能存数据',却忽略了它的内存布局和扩容逻辑,这往往导致代码隐性性能问题。今天咱们从基础到实践,把这两个核心点讲透,最后还会给可直接复用的优化方案。
一、Vec 的内存布局是怎样的?
Vec 的内存分布很'聪明',分成栈上的控制区和堆上的存储区,两者协同工作——这是理解它所有特性的基础。

1. 栈上:3 个固定大小的'控制字段'
不管 Vec 存多少元素,它在栈上永远只占 3 个字段的空间(64 位系统下共 24 字节),定义大概长这样(简化版):
struct Vec<T> {
ptr: *mut T, // 指向堆内存的指针(非空)
len: usize, // 当前已存元素的个数
cap: usize, // 堆内存能容纳的最大元素个数(容量)
}
比如 Vec<i32>,栈上就是'指针(8 字节)+ 长度(8 字节)+ 容量(8 字节)',和里面存 1 个还是 100 个 i32 无关。
2. 堆上:连续的元素存储区
真正的元素数据存在堆上,且是连续内存——这也是 Vec 能像数组一样随机访问(O(1) 时间)的原因。举个例子:当你写 let mut v = vec![1, 2, 3] 时,内存布局是这样的:
- 栈上:
ptr指向堆上一块能存 3 个i32的内存,len=3,cap=3; - 堆上:连续存储
1、2、3(共 12 字节,每个i32占 4 字节)。
3. 特殊情况:零大小类型(ZST)的优化
如果存的是 Vec<()>(() 是零大小类型,不占内存),Rust 会做特殊优化:堆上不分配任何内存,ptr 会指向一个固定的'空指针占位符',cap 可以无限大(因为不需要实际存储)。比如 Vec<()> 用来计数时,比 Vec<u8> 更省内存——这是 Rust 对边缘场景的精细化优化,实际开发中处理'无数据但需计数'的场景很有用。
二、Vec 的扩容策略是怎么运作的?
当你用 push 或 extend 添加元素时,一旦 len == cap(当前元素数等于容量),Vec 就会触发扩容。这个过程看似简单,实则藏着'时间与空间的权衡'。




