Vec<T> in Rust: Memory Management & vs &[T] Guide

Max WellsMax WellsFounder of Rustify

TL;DR: Vec<T> is Rust's growable, heap-allocated array, the equivalent of std::vector in C++ or ArrayList in Java. It stores elements contiguously in memory, grows by doubling capacity when full, and is the most commonly used collection in Rust. Vec<T> owns its data, when it goes out of scope, the memory is freed. A &[T] (slice) is a borrowed view into a Vec or any contiguous sequence, and is what you use in function parameters when you only need to read.


What Is Vec<T> in Rust?

Vec<T> is a heap-allocated, growable array that owns its elements ; Rust's go-to collection for ordered sequences of values.

fn main() {
    // Create
    let mut v: Vec<i32> = Vec::new();
    let v2 = vec![1, 2, 3, 4, 5]; // macro shorthand
 
    // Push and pop
    v.push(10);
    v.push(20);
    v.push(30);
    let last = v.pop(); // Some(30)
 
    // Access; panics on out-of-bounds
    println!("{}", v[0]); // 10
 
    // Safe access; returns Option
    if let Some(x) = v.get(1) {
        println!("{x}"); // 20
    }
 
    println!("Length: {}", v.len());
    println!("Capacity: {}", v.capacity()); // may be > len
}

How Does Vec Manage Memory?

Vec maintains three values: a pointer to heap memory, the current length, and the allocated capacity. When length reaches capacity, it reallocates with double the space.

fn main() {
    let mut v: Vec<i32> = Vec::new();
    println!("len={}, cap={}", v.len(), v.capacity()); // 0, 0
 
    v.push(1);
    println!("len={}, cap={}", v.len(), v.capacity()); // 1, 4 (initial alloc)
 
    // Pre-allocate when you know the size; avoids repeated reallocations
    let mut v = Vec::with_capacity(1000);
    for i in 0..1000 {
        v.push(i); // no reallocations
    }
 
    // Shrink excess capacity
    v.shrink_to_fit();
 
    // Release memory explicitly
    drop(v); // or let it go out of scope
}

Each reallocation copies all elements, with_capacity avoids this when the final size is known.


How Do You Iterate Over a Vec?

Use for loops with .iter() (borrow), .iter_mut() (mutable borrow), or .into_iter() (consume), or chain iterator adapters.

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
 
    // Immutable iteration
    for n in &numbers {
        print!("{n} ");
    }
 
    // Mutable iteration
    let mut values = vec![1, 2, 3];
    for v in &mut values {
        *v *= 2; // dereference to modify
    }
    println!("{:?}", values); // [2, 4, 6]
 
    // Iterator adapters
    let doubled: Vec<i32> = numbers.iter().map(|&x| x * 2).collect();
    let evens: Vec<&i32>  = numbers.iter().filter(|&&x| x % 2 == 0).collect();
    let sum: i32          = numbers.iter().sum();
 
    println!("doubled: {:?}", doubled);
    println!("evens: {:?}", evens);
    println!("sum: {sum}");
}

What Is the Difference Between Vec<T> and &[T]?

Vec<T> owns its data. &[T] (a slice) is a borrowed view into any contiguous sequence, a Vec, an array, or part of either. Use &[T] in function parameters for maximum flexibility.

// Accepts Vec, arrays, and slices; most flexible
fn sum(numbers: &[i32]) -> i32 {
    numbers.iter().sum()
}
 
fn main() {
    let v = vec![1, 2, 3, 4, 5];
    let arr = [1, 2, 3, 4, 5];
 
    println!("{}", sum(&v));          // Vec coerces to &[i32]
    println!("{}", sum(&arr));        // array coerces to &[i32]
    println!("{}", sum(&v[1..3]));    // slice of Vec
}

&[T] is two words: a pointer and a length. It has no capacity and cannot grow. It is the idiomatic way to pass sequences to functions you don't need to own or modify.


Common Vec Operations

fn main() {
    let mut v = vec![3, 1, 4, 1, 5, 9, 2, 6];
 
    // Sort
    v.sort();                          // [1, 1, 2, 3, 4, 5, 6, 9]
    v.sort_by(|a, b| b.cmp(a));       // descending
    v.sort_by_key(|&x| x % 3);        // by remainder
 
    // Dedup (removes consecutive duplicates; sort first)
    v.sort();
    v.dedup();                         // [1, 2, 3, 4, 5, 6, 9]
 
    // Retain only elements matching predicate
    v.retain(|&x| x % 2 == 0);        // [2, 4, 6]
 
    // Extend from another iterator
    v.extend([8, 10, 12]);
 
    // Drain; remove a range and iterate over removed items
    let removed: Vec<i32> = v.drain(0..2).collect();
 
    // Truncate to length
    v.truncate(3);
 
    // Clear all elements (keeps allocation)
    v.clear();
 
    // Concatenate vecs
    let a = vec![1, 2];
    let mut b = vec![3, 4];
    b.extend(a.iter());    // [3, 4, 1, 2]
}

Frequently Asked Questions

Use arrays when the size is fixed and known at compile time, they live on the stack and have no allocation overhead. Use Vec when the size varies at runtime or is not known at compile time. Arrays are slightly faster for small, fixed data; Vec is more flexible.

.remove(i) removes the element at index i and shifts all subsequent elements left ; O(n). .swap_remove(i) swaps the element with the last element and pops ; O(1) but changes order. Choose based on whether order matters.

let strings = vec!["hello".to_string(), "world".to_string()];
let strs: Vec<&str> = strings.iter().map(String::as_str).collect();

A heterogeneous collection, a Vec of owned trait objects. Use this when you need to store different concrete types that all implement the same trait in one collection. Each element is heap-allocated and accessed via dynamic dispatch.


Sources


  • Ownership: Vec owns its elements; passing it moves ownership
  • Iterator: Vec is the most common iterator source
  • Generic: Vec<T> is generic over element type T
  • String: String is internally a Vec<u8>
  • HashMap: Vec and HashMap are the two foundational Rust collections to compare early
  • Rand: Random sampling and shuffling frequently operate on Vec<T>
  • Slice: Borrowed slices are the zero-copy view into Vec data

Keep Reading

Ready to Land a $80-120k Rust Job?