Skip to content
Learn Rust
Theme

Values, the Stack and the Heap

Ownership · Lesson 15

A Rust variable holds a value itself, not an arrow to an object somewhere else. Where that value lives, on the stack or on the heap, is mostly decided by its type. This lesson explains both places, so every later lesson can use the words.

Coming from GoA Go struct variable holds its fields directly, and so does Rust's. The difference is who picks the place. Go's escape analysis moves a value to the heap for you when it has to outlive the function; in Rust the type says: a plain value lives in its variable, and Box, Vec and String own heap memory. A Vec's header is the same three words as a Go slice header.

Coming from KotlinA Kotlin class variable is a reference to an object (normally on the JVM heap), so val b = a shares the object. A Rust variable holds the value itself, like a C struct, so let b = a copies the value (or moves it), and no second name for one object appears unless you ask for one with &, or with Rc or Arc when several owners need the value.

Coming from PythonIn Python every variable is a name for an object, so b = a gives one object two names. In Rust a variable holds the value itself. let b = a copies the value (for small plain types) or moves it. A second name for the same value is a reference, written &a, or an Rc/Arc when there are several owners; you ask for it.

Coming from TypeScriptObjects in JavaScript and TypeScript are always held by reference, so const b = a shares the object (const fixes the binding, not the object). A Rust variable holds the value itself, so let b = a copies it or moves it. To share, you write &a, or use Rc/Arc for several owners.

Coming from JavaA Java object variable is a reference to an object (normally on the heap), so Point b = a shares it. A Rust variable holds the value itself, closer to a Java int than to a Java Point: let b = a copies it or moves it, and sharing is something you ask for with &, or with Rc/Arc for several owners.

Coming from C#Rust's variables work like C# struct variables: the value sits in the variable, and var b = a copies it. There is no class kind that shares by default. A reference is a separate thing you write with &, and the compiler checks it.

Coming from CYou already know this: a Rust struct variable is a C struct on the stack, and Box is a pointer to heap memory that frees itself. What Rust adds is checking. It knows which variable owns each heap allocation, never frees one twice, and frees it when the owner goes out of scope. It also won't let a reference outlive what it points at.

Coming from C++Same model as yours: locals live in the frame, and Box<T> is close to std::unique_ptr<T>. Rust adds two things: moves are the default and the old name can't be used afterwards, and the compiler rejects a reference that would outlive its value.

The quick comparison

Rustsrc/main.rsRun ↗
#[derive(Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let mut b = a; // b holds its own copy of the two numbers
b.x = 99;
println!("a = ({}, {})", a.x, a.y);
println!("b = ({}, {})", b.x, b.y);
}
$ cargo run
a = (1, 2)
b = (99, 2)
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
a := Point{1, 2}
b := a // a struct is a value: b is a copy
b.X = 99
fmt.Printf("a = (%d, %d)\n", a.X, a.Y)
fmt.Printf("b = (%d, %d)\n", b.X, b.Y)
// To share, you take an address on purpose.
p := &a
p.X = 50
fmt.Printf("a after p.X = 50: (%d, %d)\n", a.X, a.Y)
}
$ go run main.go
a = (1, 2)
b = (99, 2)
a after p.X = 50: (50, 2)
class Point(var x: Int, var y: Int) {
override fun toString() = "($x, $y)"
}
fun main() {
val a = Point(1, 2)
val b = a // b is another name for the same object
b.x = 99
println("a = $a")
println("b = $b")
// To get a separate point, you build one.
val c = Point(a.x, a.y)
c.x = 7
println("a after c.x = 7: $a")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
a = (99, 2)
b = (99, 2)
a after c.x = 7: (99, 2)
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"({self.x}, {self.y})"
a = Point(1, 2)
b = a # b is another name for the same object
b.x = 99
print("a =", a)
print("b =", b)
# To get a separate point, you build one.
c = Point(a.x, a.y)
c.x = 7
print("a after c.x = 7:", a)
$ python3 main.py
a = (99, 2)
b = (99, 2)
a after c.x = 7: (99, 2)
interface Point {
x: number;
y: number;
}
const a: Point = { x: 1, y: 2 };
const b = a; // b is another name for the same object
b.x = 99;
console.log(`a = (${a.x}, ${a.y})`);
console.log(`b = (${b.x}, ${b.y})`);
// To get a separate point, you build one.
const c: Point = { ...a };
c.x = 7;
console.log(`a after c.x = 7: (${a.x}, ${a.y})`);
$ tsc --strict --module nodenext --types node main.ts && node main.js
a = (99, 2)
b = (99, 2)
a after c.x = 7: (99, 2)
public class Main {
static class Point {
int x, y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = a; // b is another name for the same object
b.x = 99;
System.out.println("a = " + a);
System.out.println("b = " + b);
// To get a separate point, you build one.
Point c = new Point(a.x, a.y);
c.x = 7;
System.out.println("a after c.x = 7: " + a);
}
}
$ javac Main.java && java Main
a = (99, 2)
b = (99, 2)
a after c.x = 7: (99, 2)
var a = new PointClass { X = 1, Y = 2 };
var b = a; // a class: b is another name for the same object
b.X = 99;
Console.WriteLine($"class: a.X = {a.X}, b.X = {b.X}");
var s = new PointStruct { X = 1, Y = 2 };
var t = s; // a struct: t is a copy
t.X = 99;
Console.WriteLine($"struct: s.X = {s.X}, t.X = {t.X}");
class PointClass
{
public int X, Y;
}
struct PointStruct
{
public int X, Y;
}
$ dotnet run
class:  a.X = 99, b.X = 99
struct: s.X = 1, t.X = 99
#include <stdio.h>
struct Point {
int x, y;
};
int main(void) {
struct Point a = {1, 2};
struct Point b = a; // a struct is a value: b is a copy
b.x = 99;
printf("a = (%d, %d)\n", a.x, a.y);
printf("b = (%d, %d)\n", b.x, b.y);
// To share, you take an address on purpose.
struct Point *p = &a;
p->x = 50;
printf("a after p->x = 50: (%d, %d)\n", a.x, a.y);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
a = (1, 2)
b = (99, 2)
a after p->x = 50: (50, 2)
#include <iostream>
struct Point {
int x, y;
};
int main() {
Point a{1, 2};
Point b = a; // a struct is a value: b is a copy
b.x = 99;
std::cout << "a = (" << a.x << ", " << a.y << ")\n";
std::cout << "b = (" << b.x << ", " << b.y << ")\n";
// To share, you take a reference on purpose.
Point &r = a;
r.x = 50;
std::cout << "a after r.x = 50: (" << a.x << ", " << a.y << ")\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
a = (1, 2)
b = (99, 2)
a after r.x = 50: (50, 2)

What's different

  • The stack holds function calls' local variables, and is cleaned up when each call returns. The heap holds data whose size isn't known when the program is compiled, or that must outlive the call that made it, among other things.
  • A String or Vec is two parts: a small header (a pointer, a length and a capacity) in the variable, and the text or elements on the heap.
  • Numbers, and a small struct that derives Copy, are copied by let b = a. Types that own heap memory are moved: the header is copied and the old name stops working.
  • size_of::<T>() tells you how many bytes a type takes in its variable. A &str is 16 bytes (a pointer and a length) however long its text is.

The toolkit

Later lessons say “this lives on the stack” or “this frees the heap memory”. This lesson explains those words once. It also answers the question that comes before them: when you write let b = a, what does a Rust variable hold?

Sizes come from Linux on a 64-bit ARM machine.

A variable holds the value

In Java, Kotlin, Python, TypeScript and C# classes, a variable that holds an object holds a reference: an arrow to an object that lives somewhere else. b = a copies the arrow, so two names lead to one object. The quick comparison at the top of this page showed it: change b, and a changes too.

In Rust a variable holds the value itself, as a C struct, a Go struct or a C# struct already does. let b = a makes b hold its own copy of the bytes:

Rustsrc/main.rsRun ↗
// `Copy` is a label that makes `let b = a` copy the bytes. More on it below.
#[derive(Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let mut b = a;
b.x = 99;
println!("a: x = {}, y = {}", a.x, a.y);
println!("b: x = {}, y = {}", b.x, b.y);
println!("size of a: {} bytes", size_of_val(&a));
println!("size of b: {} bytes", size_of_val(&b));
}
$ cargo run
a: x = 1, y = 2
b: x = 99, y = 2
size of a: 8 bytes
size of b: 8 bytes

a and b are separate, 8 bytes each (two i32s). Changing b.x didn’t touch a, because there is no arrow. (#[derive(Clone, Copy)] is a label that makes Point copy bit for bit on let b = a; below shows what happens without it.)

let a = Point { x: 1, y: 2 }; let mut b = a; b.x = 99;
STACKa: Pointx 1y 2b: Pointx 99y 2
Two variables, each holding its own two numbers. There is no heap and no arrow.

For some types the value is a small header that points at the data. A String variable holds a pointer, a length and a capacity, and owns the text it points to.

Rust does have references, written & (below), and Rc for several owners of one value. You ask for both; nothing is shared unless you do.

The stack

A running program keeps most of its data in two places. The first is the stack.

Every function call gets a frame: a block of memory for its parameters and local variables. When the function returns, its frame is given back. Frames are added and removed at the same end, like a stack of plates.

This makes the stack cheap: no allocator has to search for room, and giving a frame back is just moving one pointer. (Destructors of locals still run.) The price is that everything in a frame has a size the compiler knows at compile time; it reserves what it needs.

Rustsrc/main.rsRun ↗
fn square(n: i32) -> i32 {
let result = n * n;
println!(" in square: n = {n}, result = {result}");
result
}
fn sum_of_squares(a: i32, b: i32) -> i32 {
println!("in sum_of_squares: a = {a}, b = {b}");
let first = square(a);
let second = square(b);
first + second
}
fn main() {
let x = 3;
let y = 4;
println!("in main: x = {x}, y = {y}");
let total = sum_of_squares(x, y);
println!("in main: total = {total}");
}
$ cargo run
in main: x = 3, y = 4
in sum_of_squares: a = 3, b = 4
  in square: n = 3, result = 9
  in square: n = 4, result = 16
in main: total = 25

main calls sum_of_squares, which calls square twice. Each call has its own n and result: the first square frame was gone before the second began.

While square(4) is running
STACKx: i323in main's framey: i324in main's framea: i323in sum_of_squares's frameb: i324in sum_of_squares's framefirst: i329in sum_of_squares's framen: i324in square's frameresult: i3216in square's frame
Three frames, the newest last. When square returns, its frame is gone.

This is the model to think in. The real machine is cleverer: the compiler may keep a variable in a CPU register (a storage slot inside the processor), remove it, or merge a small function into its caller. None of that changes what your program does.

The heap

The second place is the heap, a large pool of memory the program asks for while it runs. You ask an allocator (the code that hands out heap memory) for some bytes; it finds a free spot that big and gives you a pointer, the address of that spot. When you are finished, the memory is given back for reuse.

Two common reasons to use the heap are:

  • The size isn’t known at compile time: the text a user types, or the items in a list that grows.
  • The data must outlive the function that made it. A frame is gone when its function returns; heap memory stays until it is given back.

Heap data costs more than the stack: you follow a pointer, and Rust’s allocator is a malloc-style one that has to find a free spot of the right size (std::alloc). Garbage-collected runtimes allocate differently. The Rust Book’s stack and heap section has more.

String, Vec and Box own heap memory. A &str is text you can read but don’t own; a String is text you own and can grow (the Strings lesson has the full story). A String is a small fixed-size header in the variable (a pointer to the text, the length in bytes and the capacity reserved) plus the text on the heap:

Rustsrc/main.rsRun ↗
fn main() {
let short = String::from("hello");
let long = "x".repeat(1000);
println!(
"short: {} bytes of text, variable takes {} bytes",
short.len(),
size_of_val(&short)
);
println!(
"long: {} bytes of text, variable takes {} bytes",
long.len(),
size_of_val(&long)
);
println!("capacity of short: {}", short.capacity());
// An array keeps its elements inside the variable, so its size grows with them.
let small_array = [1u8; 5];
let big_array = [1u8; 1000];
println!("array of 5 bytes takes {} bytes", size_of_val(&small_array));
println!(
"array of 1000 bytes takes {} bytes",
size_of_val(&big_array)
);
}
$ cargo run
short: 5 bytes of text, variable takes 24 bytes
long:  1000 bytes of text, variable takes 24 bytes
capacity of short: 5
array of 5 bytes takes 5 bytes
array of 1000 bytes takes 1000 bytes

Both String variables take 24 bytes, whether the text is 5 bytes or 1,000. An array variable grows with its elements, because it keeps them inside. That is the key difference between a fixed-size value and one that owns heap memory.

let s = String::from("hello");
STACKHEAPs: Stringpointerlength 5capacity 55 bytes reservedhello
The header is on the stack and has the same size however long the text is. The arrow is the pointer. When s goes out of scope, the heap bytes are given back.

Who gives the heap bytes back, and when, is what ownership is about. In short, the variable that owns the memory gives it back when it goes out of scope, at the end of its { } block. You never write the call.

Copy values and moved values

Now let b = a has a precise meaning: the variable’s bytes are copied into b. The two cases differ in what those bytes are.

For a number, the bytes are the whole value, so b is an independent copy and both names stay usable. Such a type is called Copy. Point is Copy only because of its derive line. Take the line away and it is moved like a String, as the compiler shows:

Rustsrc/main.rs✗ won't compile
// The same Point as above, but without #[derive(Clone, Copy)].
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let b = a;
println!("b = ({}, {})", b.x, b.y);
println!("a = ({}, {})", a.x, a.y); // a was moved into b
}
$ cargo buildWhat is E0382?
error[E0382]: borrow of moved value: `a`
  --> src/main.rs:11:35
   |
 8 |     let a = Point { x: 1, y: 2 };
   |         - move occurs because `a` has type `Point`, which does not implement the `Copy` trait
 9 |     let b = a;
   |             - value moved here
10 |     println!("b = ({}, {})", b.x, b.y);
11 |     println!("a = ({}, {})", a.x, a.y); // a was moved into b
   |                                   ^^^ value borrowed here after move
   |
note: if `Point` implemented `Clone`, you could clone the value
  --> src/main.rs:2:1
   |
 2 | struct Point {
   | ^^^^^^^^^^^^ consider implementing `Clone` for this type
...
 9 |     let b = a;
   |             - you could clone this value

For a type that owns heap memory, the bytes are only the header. In Python, b = a on a string just works. In Rust it does not:

Rustsrc/main.rs✗ won't compile
fn main() {
let a = String::from("hello");
let b = a;
println!("b = {b}");
println!("a = {a}");
}
$ cargo buildWhat is E0382?
error[E0382]: borrow of moved value: `a`
 --> src/main.rs:5:20
  |
2 |     let a = String::from("hello");
  |         - move occurs because `a` has type `String`, which does not implement the `Copy` trait
3 |     let b = a;
  |             - value moved here
4 |     println!("b = {b}");
5 |     println!("a = {a}");
  |                    ^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable
  |
3 |     let b = a.clone();
  |              ++++++++

Two copies of the header would hold the same pointer, so both would try to give the text back at the end of scope. That is a double free: the same memory returned twice, which corrupts the allocator’s bookkeeping and is a classic source of security holes. So Rust moves the String: the header is copied to b, the heap data stays where it is, and a can no longer be used. This program shows both cases:

Rustsrc/main.rsRun ↗
fn main() {
// A Copy value: the bits are copied, and both variables stay usable.
let n = 7;
let m = n;
println!("n = {n}, m = {m}");
// A String: only the header moves. The text stays where it was.
let a = String::from("hello");
let text_before = a.as_ptr();
let b = a; // `a` can't be used after this line
println!("b = {b}");
println!("len {}, capacity {}", b.len(), b.capacity());
println!("text stayed where it was: {}", b.as_ptr() == text_before);
}
$ cargo run
n = 7, m = 7
b = hello
len 5, capacity 5
text stayed where it was: true

n and m both print. For the String, the last line shows that the text did not move: b’s pointer is the address a had. Only the 24-byte header was copied.

let a = String::from("hello"); let b = a;
STACKHEAPa: String (moved)pointerlength 5capacity 5not usable: b owns the text nowb: Stringpointerlength 5capacity 5hello
The header was copied and the text was not. a's old bytes may still be there, but the compiler won't let you read them, and only b will free the text.

Which types are Copy, and what to do instead of moving, are the subject of Copy or reference? and then Ownership.

How big is each type?

size_of tells you how many bytes a type takes in its variable (::<i32> names the type to measure). That shows which types are plain values and which are headers:

Rustsrc/main.rsRun ↗
use std::mem::size_of;
// We only measure this struct, so silence the "fields never read" warning.
#[allow(dead_code)]
struct Pair {
x: i32,
y: i32,
}
fn main() {
println!("type bytes");
println!("i32 {:>5}", size_of::<i32>());
println!("u64 {:>5}", size_of::<u64>());
println!("f64 {:>5}", size_of::<f64>());
println!("bool {:>5}", size_of::<bool>());
println!("char {:>5}", size_of::<char>());
println!("(i32, i32) {:>5}", size_of::<(i32, i32)>());
println!("[u8; 16] {:>5}", size_of::<[u8; 16]>());
println!("Pair {:>5}", size_of::<Pair>());
println!("&i32 {:>5}", size_of::<&i32>());
println!("&str {:>5}", size_of::<&str>());
println!("String {:>5}", size_of::<String>());
println!("Vec<i32> {:>5}", size_of::<Vec<i32>>());
println!("Box<i32> {:>5}", size_of::<Box<i32>>());
}
$ cargo run
type               bytes
i32                    4
u64                    8
f64                    8
bool                   1
char                   4
(i32, i32)             8
[u8; 16]              16
Pair                   8
&i32                   8
&str                  16
String                24
Vec<i32>              24
Box<i32>               8

Read down the table:

Type Bytes What those bytes are
i32, u64, f64, bool, char 4, 8, 8, 1, 4 the value itself
(i32, i32), [u8; 16], Pair 8, 16, 8 the fields or elements, side by side
&i32 8 a pointer
&str 16 a pointer and a length, however long the text is
String, Vec<i32> 24, 24 pointer, length and capacity: three 8-byte numbers
Box<i32> 8 a pointer to one i32 on the heap

A Vec<i32> is 24 bytes whether it holds three items or three million, because the items aren’t in it. Pointers are 8 bytes on this 64-bit machine.

References and Box

Two more kinds of value hold an address instead of the data.

  • A reference, written &x, is the address of a value that lives elsewhere; taking one doesn’t copy the value. It is a pointer-sized variable (8 bytes here; a reference to a str or a slice also carries a length).
  • A Box<T> puts one T on the heap and holds the pointer.
Rustsrc/main.rsRun ↗
use std::ptr;
fn main() {
let x = 42;
let r = &x; // a reference to x: the address of x, not a copy
println!("r points at x: {}", ptr::eq(r, &x));
println!("*r: {}", *r);
let b = Box::new(x); // allocates 4 bytes on the heap and copies x into them
println!("*b: {}", *b);
println!("b points at x: {}", ptr::eq(&*b, &x));
println!("size of x: {} bytes", size_of_val(&x));
println!("size of r: {} bytes", size_of_val(&r));
println!("size of b: {} bytes", size_of_val(&b));
println!("size of what b points at: {} bytes", size_of_val(&*b));
}
$ cargo run
r points at x: true
*r: 42
*b: 42
b points at x: false
size of x: 4 bytes
size of r: 8 bytes
size of b: 8 bytes
size of what b points at: 4 bytes

ptr::eq asks whether two references point to the same address. It is true for r and x: r points at x itself. It is false for b and x: Box::new(x) copied the 42 into new heap memory. (*r and *b mean “the value this points at”.)

let x = 42; let r = &x; let b = Box::new(x);
STACKHEAPx: i3242r: &i32address of xpoints at x above; the diagram only draws arrows to the heapb: Box<i32>pointera copy of x42
r is a second way to reach x. b owns a separate copy of 42 on the heap.

A reference does not own what it points at, which is why handing a function a &String is cheap. The rules for when a reference may exist are the borrowing lesson.

Going deeper

Stack overflow, for real

The stack has a fixed size. For the main thread on Linux it is typically 8 MiB; the limit can be changed, and threads you spawn get a different default. If the frames need more, the program is stopped. The usual cause is recursion that goes too deep:

Rustsrc/main.rs! exits with code -6Run ↗
// Each call to `depth` gets its own frame, and none of them returns
// until the one below it does. The frames pile up.
fn depth(n: u64) -> u64 {
if n == 0 { 0 } else { 1 + depth(n - 1) }
}
fn main() {
println!("depth(1_000): {}", depth(1_000));
println!("depth(100_000_000): {}", depth(100_000_000));
}
$ cargo run
depth(1_000): 1000

thread 'main' (1) has overflowed its stack
fatal runtime error: stack overflow, aborting

depth(1_000) works. In a debug build (as on this page) every frame stays alive, and a hundred million frames don’t fit. An optimised build may turn this recursion into a loop, so don’t rely on the result. The output is not a normal panic: there is no panicked at line, only the two lines about the overflow, and the process aborts. A large local array lives in its frame too, so tens of millions of bytes would not fit in 8 MiB either. Use a Vec for large data and a loop instead of deep recursion.

How other languages compare

Language A struct or object variable holds Heap memory is freed by
Rust the struct itself; Box, Vec and String hold a pointer the owner, when it goes out of scope
C, C++ the struct itself; malloc, new, std::vector and std::string own heap memory (a short std::string may be stored inline) you (free, delete) or a destructor
Go the struct itself; the compiler picks stack or heap (escape analysis) the garbage collector
Java, Kotlin a reference to an object the garbage collector
C# a struct: the value. A class: a reference, usually to a heap object, though .NET 10 can stack-allocate one that doesn’t escape the garbage collector
Python, TypeScript a reference to an object (in Python even an int is an object) CPython: reference counting plus a cycle collector; JavaScript: a collector

In Go, Java, Python and TypeScript the same code works whichever place the runtime picks. C# lets you choose with struct or class. Rust also lets you choose, in the types, and in return the compiler knows who owns each heap allocation. So (in safe code) it never frees one twice, frees it at a point fixed by the code, and rejects a reference that would outlive its value.

Padding and alignment (optional)

A struct’s size is not always the sum of its fields. Processors read most values fastest, or only, from addresses that are a multiple of a number that depends on the type (for a plain number, usually its size). That number is the type’s alignment. The compiler adds unused padding bytes to line fields up.

Rustsrc/main.rsRun ↗
use std::mem::{align_of, size_of};
// We only measure these structs, so the fields are never read.
// #[repr(C)] keeps the fields in the order written, as a C compiler would.
#[allow(dead_code)]
#[repr(C)]
struct MixedC {
a: u8,
n: u32,
b: u8,
}
#[allow(dead_code)]
#[repr(C)]
struct SmallFirstC {
a: u8,
b: u8,
n: u32,
}
fn main() {
println!(" size align");
println!(
"u32 {:>4} {:>5}",
size_of::<u32>(),
align_of::<u32>()
);
println!(
"[u8; 16] {:>4} {:>5}",
size_of::<[u8; 16]>(),
align_of::<[u8; 16]>()
);
println!(
"MixedC {:>4} {:>5}",
size_of::<MixedC>(),
align_of::<MixedC>()
);
println!(
"SmallFirstC {:>4} {:>5}",
size_of::<SmallFirstC>(),
align_of::<SmallFirstC>()
);
}
$ cargo run
            size  align
u32            4      4
[u8; 16]      16      1
MixedC        12      4
SmallFirstC    8      4

MixedC is a (1 byte), 3 bytes of padding so n starts at a multiple of 4, n (4), b (1) and 3 more of padding: 12. SmallFirstC keeps the small fields together and needs 2 bytes of padding: 8. Alignment is not size: [u8; 16] is 16 bytes with alignment 1.

#[repr(C)] keeps fields in the order written, as a C compiler would. Without it, Rust may reorder fields, and the Reference promises no particular layout, so don’t rely on a size.

Where string literals live

The text of a literal like "hello" is, in the words of the Rust Book, “hardcoded directly into the final executable”.

Rustsrc/main.rsRun ↗
// The frame of `greeting` is gone when it returns, but the text it hands back
// is not in the frame, so it is still there.
fn greeting() -> &'static str {
"hello"
}
fn main() {
let g = greeting();
println!("g: {g}");
// The variable `g` is a pointer and a length on the stack.
println!("size of g: {} bytes", size_of_val(&g));
// The text it points at is 5 bytes of the program's own data.
println!("size of the text: {} bytes", size_of_val(g));
}
$ cargo run
g: hello
size of g: 16 bytes
size of the text: 5 bytes

greeting’s frame is gone when it returns, yet g works: the text was never in the frame, and 'static means it lasts as long as the program. g is a &str, a pointer and a length (16 bytes), pointing at 5 bytes that belong to the program.

Literals are fast because the text is fixed at compile time. You can’t change one because &str gives read-only access to bytes that belong to the program. String::from("hello") copies them onto the heap, and the String owns the copy.

Practice

Try it

Each exercise is a program with checks in main. Open it in the Playground, replace the todo!() (a placeholder that stops the program with your hint), and run it until it prints all checks passed. The &Vec<i32> in the vec-header signatures is a reference: the function looks at the Vec without taking it (borrowing).

Copy or move?

shifted takes a Copy point, so the caller keeps its own. with_len takes a String, which is moved in, so it must hand the text back.

Starterexercises/move-or-copy.rs
// `Copy` makes `let b = a` copy the bytes, so `a` stays usable.
#[derive(Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
/// Returns a point `dx` to the right of `p`. `p` is `Copy`, so the caller keeps its own.
fn shifted(p: Point, dx: i32) -> Point {
todo!("make a copy with `let mut q = p;`, change q.x, and return q")
}
/// Takes the text (it is moved in) and hands it back with its length in bytes,
/// so the caller can keep using it.
fn with_len(text: String) -> (String, usize) {
todo!("get the length first, then build the tuple: `(text, text.len())` would not compile")
}
fn main() {
let p = Point { x: 1, y: 2 };
let q = shifted(p, 10);
assert_eq!((p.x, p.y), (1, 2)); // p is untouched, and still usable
assert_eq!((q.x, q.y), (11, 2));
let text = String::from("héllo");
let (text, n) = with_len(text); // the old `text` was moved; this is a new one
assert_eq!(text, "héllo");
assert_eq!(n, 6); // "é" takes two bytes
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/move-or-copy.solution.rsRun ↗
// `Copy` makes `let b = a` copy the bytes, so `a` stays usable.
#[derive(Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
/// Returns a point `dx` to the right of `p`. `p` is `Copy`, so the caller keeps its own.
fn shifted(p: Point, dx: i32) -> Point {
let mut q = p;
q.x += dx;
q
}
/// Takes the text (it is moved in) and hands it back with its length in bytes,
/// so the caller can keep using it.
fn with_len(text: String) -> (String, usize) {
let n = text.len();
(text, n)
}
fn main() {
let p = Point { x: 1, y: 2 };
let q = shifted(p, 10);
assert_eq!((p.x, p.y), (1, 2)); // p is untouched, and still usable
assert_eq!((q.x, q.y), (11, 2));
let text = String::from("héllo");
let (text, n) = with_len(text); // the old `text` was moved; this is a new one
assert_eq!(text, "héllo");
assert_eq!(n, 6); // "é" takes two bytes
println!("all checks passed");
}
$ cargo run
all checks passed

Predict the sizes

Predict the size in bytes of seven types on a 64-bit machine. Two are references or boxes, where the answer isn’t the size of what they point at.

Starterexercises/sizes.rs
use std::mem::size_of;
/// Bytes taken by each type on a 64-bit machine, in this order:
/// u8, i32, (i32, i32), [u16; 5], &str, Box<String>, Vec<u8>
fn predictions() -> [usize; 7] {
todo!("work out each size, then write the seven numbers")
}
fn main() {
let actual = [
size_of::<u8>(),
size_of::<i32>(),
size_of::<(i32, i32)>(),
size_of::<[u16; 5]>(),
size_of::<&str>(),
size_of::<Box<String>>(),
size_of::<Vec<u8>>(),
];
assert_eq!(predictions(), actual);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/sizes.solution.rsRun ↗
use std::mem::size_of;
/// Bytes taken by each type on a 64-bit machine, in this order:
/// u8, i32, (i32, i32), [u16; 5], &str, Box<String>, Vec<u8>
fn predictions() -> [usize; 7] {
// A Box is one pointer, however big the String it points at is.
[1, 4, 8, 10, 16, 8, 24]
}
fn main() {
let actual = [
size_of::<u8>(),
size_of::<i32>(),
size_of::<(i32, i32)>(),
size_of::<[u16; 5]>(),
size_of::<&str>(),
size_of::<Box<String>>(),
size_of::<Vec<u8>>(),
];
assert_eq!(predictions(), actual);
println!("all checks passed");
}
$ cargo run
all checks passed

The header and the heap

Write three functions about a Vec<i32>: the heap bytes its elements reserve, the bytes its header takes, and its spare room. The checks show that the header never changes size.

Starterexercises/vec-header.rs
use std::mem::size_of;
/// How many bytes the elements of `v` take on the heap, counting spare room.
fn heap_bytes(v: &Vec<i32>) -> usize {
todo!("capacity times the size of one element")
}
/// How many bytes the variable `v` itself takes (the header), however many elements it has.
fn header_bytes(v: &Vec<i32>) -> usize {
todo!("the size of the Vec type itself, not of its elements")
}
/// How many more elements fit before the Vec has to ask for a bigger buffer.
fn spare(v: &Vec<i32>) -> usize {
todo!("capacity minus length")
}
fn main() {
let mut v: Vec<i32> = Vec::with_capacity(10);
v.push(1);
v.push(2);
v.push(3);
assert_eq!(header_bytes(&v), 3 * size_of::<usize>());
assert_eq!(spare(&v), v.capacity() - 3);
assert!(spare(&v) >= 7);
assert_eq!(heap_bytes(&v), v.capacity() * 4);
// The header stays the same size when the Vec grows.
let before = header_bytes(&v);
for i in 0..1000 {
v.push(i);
}
assert_eq!(header_bytes(&v), before);
assert!(heap_bytes(&v) >= 1003 * 4);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/vec-header.solution.rsRun ↗
use std::mem::size_of;
/// How many bytes the elements of `v` take on the heap, counting spare room.
fn heap_bytes(v: &Vec<i32>) -> usize {
v.capacity() * size_of::<i32>()
}
/// How many bytes the variable `v` itself takes (the header), however many elements it has.
fn header_bytes(_v: &Vec<i32>) -> usize {
size_of::<Vec<i32>>()
}
/// How many more elements fit before the Vec has to ask for a bigger buffer.
fn spare(v: &Vec<i32>) -> usize {
v.capacity() - v.len()
}
fn main() {
let mut v: Vec<i32> = Vec::with_capacity(10);
v.push(1);
v.push(2);
v.push(3);
assert_eq!(header_bytes(&v), 3 * size_of::<usize>());
assert_eq!(spare(&v), v.capacity() - 3);
assert!(spare(&v) >= 7);
assert_eq!(heap_bytes(&v), v.capacity() * 4);
// The header stays the same size when the Vec grows.
let before = header_bytes(&v);
for i in 0..1000 {
v.push(i);
}
assert_eq!(header_bytes(&v), before);
assert!(heap_bytes(&v) >= 1003 * 4);
println!("all checks passed");
}
$ cargo run
all checks passed

Read more