bigywater.tech
Rust · 6

数组、切片与 Vec

数组、切片和 Vec 是新手最先用到、也最容易混的三样东西。这篇用真实的编译与运行结果分清:谁在栈上、谁在堆上,&[T] 到底是什么,Vec 容量怎么增长,以及越界时编译器和运行时分别怎么反应。

16 字节
&[i32] 切片(胖指针:地址 + 长度),普通引用只有 8 字节
24 字节
Vec 本身(指针、长度、容量),元素在堆上
4→8→16→32→64
Vec 逐个 push 时的容量序列

数组

实测 · 本机执行输出
  源码:
      1 | use std::mem::size_of_val;
      2 | fn main() {
      3 |     let a = [10, 20, 30, 40, 50];
      4 |     println!("a 的类型长度 = {},占 {} 字节(5 个 i32)", a.len(), size_of_val(&a));
      5 |     let i = std::env::args().count() + 6;   // 运行时才知道:7
      6 |     println!("访问 a[{i}] ...");
      7 |     println!("{}", a[i]);
      8 | }
  编译:成功
  运行(退出码 101):
    a 的类型长度 = 5,占 20 字节(5 个 i32)
    访问 a[7] ...
    
    thread 'main' (6929057) panicked at <实验目录>/src/u6a.rs:7:20:
    index out of bounds: the len is 5 but the index is 7
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
  源码:
      1 | fn main() {
      2 |     let a = [10, 20, 30];
      3 |     println!("{}", a[5]);
      4 | }
  编译:失败
    error: this operation will panic at runtime
     --> <实验目录>/src/u6b.rs:3:20
      |
    3 |     println!("{}", a[5]);
      |                    ^^^^ index out of bounds: the length is 3 but the index is 5
      |
      = note: `#[deny(unconditional_panic)]` on by default
    
    error: aborting due to 1 previous error

切片

实测 · 本机执行输出
  源码:
      1 | use std::mem::size_of;
      2 | fn sum(xs: &[i32]) -> i32 { xs.iter().sum() }
      3 | fn main() {
      4 |     let arr = [1, 2, 3, 4, 5];
      5 |     let v = vec![1, 2, 3, 4, 5];
      6 |     println!("size_of::<&i32>()   = {} 字节(普通引用:一个地址)", size_of::<&i32>());
      7 |     println!("size_of::<&[i32]>() = {} 字节(切片:地址 + 长度)", size_of::<&[i32]>());
      8 |     println!("sum(&arr)       = {}", sum(&arr));
      9 |     println!("sum(&v)         = {}", sum(&v));
     10 |     println!("sum(&arr[1..4]) = {}", sum(&arr[1..4]));
     11 |     println!("sum(&v[..2])    = {}", sum(&v[..2]));
     12 |     let s = &v[1..3];
     13 |     println!("切片 s = {:?},s.len() = {},s 的数据地址比 v 的数据地址大 {} 字节", s, s.len(), s.as_ptr() as usize - v.as_ptr() as usize);
     14 | }
  编译:成功
  运行(退出码 0):
    size_of::<&i32>()   = 8 字节(普通引用:一个地址)
    size_of::<&[i32]>() = 16 字节(切片:地址 + 长度)
    sum(&arr)       = 15
    sum(&v)         = 15
    sum(&arr[1..4]) = 9
    sum(&v[..2])    = 3
    切片 s = [2, 3],s.len() = 2,s 的数据地址比 v 的数据地址大 4 字节
  源码:
      1 | fn main() {
      2 |     let v = vec![1, 2, 3];
      3 |     let (a, b) = (3, 2);
      4 |     println!("{:?}", &v[a..b]);
      5 | }
  编译:成功
  运行(退出码 101):
    
    thread 'main' (6929196) panicked at <实验目录>/src/u6d.rs:4:24:
    slice index starts at 3 but ends at 2
    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

胖指针的更多细节见Slice 的胖指针。

Vec 与扩容

实测 · 本机执行输出
  源码:
      1 | use std::mem::size_of;
      2 | fn main() {
      3 |     println!("size_of::<Vec<i32>>() = {} 字节(指针、长度、容量各 8 字节)", size_of::<Vec<i32>>());
      4 |     let mut v: Vec<i32> = Vec::new();
      5 |     println!("Vec::new():len={} cap={}(还没有分配堆内存)", v.len(), v.capacity());
      6 |     let mut last = v.capacity();
      7 |     for i in 0..40 {
      8 |         v.push(i);
      9 |         if v.capacity() != last { println!("push 第 {:>2} 个元素后:cap {:>2} → {:>2}", v.len(), last, v.capacity()); last = v.capacity(); }
     10 |     }
     11 |     let w: Vec<i32> = Vec::with_capacity(100);
     12 |     println!("with_capacity(100):len={} cap={}", w.len(), w.capacity());
     13 |     let mut z = vec![1, 2, 3];
     14 |     z.reserve(10); println!("vec![1,2,3] 后 reserve(10):cap={}", z.capacity());
     15 |     z.shrink_to_fit(); println!("shrink_to_fit 后:cap={}", z.capacity());
     16 | }
  编译:成功
  运行(退出码 0):
    size_of::<Vec<i32>>() = 24 字节(指针、长度、容量各 8 字节)
    Vec::new():len=0 cap=0(还没有分配堆内存)
    push 第  1 个元素后:cap  0 →  4
    push 第  5 个元素后:cap  4 →  8
    push 第  9 个元素后:cap  8 → 16
    push 第 17 个元素后:cap 16 → 32
    push 第 33 个元素后:cap 32 → 64
    with_capacity(100):len=0 cap=100
    vec![1,2,3] 后 reserve(10):cap=13
    shrink_to_fit 后:cap=3

扩容的实现细节与成本见Vec 扩容机制。

栈与堆

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    局部变量地址   = 0x16fd726ff
    数组元素地址   = 0x16fd72700(与局部变量相差 1 字节)
    Vec 元素地址   = 0x100795300(与局部变量相差 1868420095 字节)
    Vec 本身(三个字)地址 = 0x16fd72710(在栈上)

地址是每次运行都会变的数字,这里只看「相差多远」。栈与堆的更多讨论见Rust 栈与堆分配。

get

实测 · 本机执行输出
  编译:成功
  运行(退出码 0):
    v.get(1) = Some(20)
    v.get(9) = None
    v.get(1..3) = Some([20, 30])
    v.get(2..9) = None
    v.first() = Some(10),v.last() = Some(30)

分步演示

数组、切片与 Vec
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。64 位目标;扩容序列是 rustc 1.98 标准库当前的行为,不是语言规范保证的;地址为单次运行的数字
    • 没有验证:零长度类型的 Vec、Vec 在 release 下的扩容序列是否相同、大数组放在栈上导致栈溢出的行为、切片模式匹配、多维数组
    京ICP备2026067502号-1京公网安备11010502063487号