Skip to content

What an address shows

Level: 201 · working knowledge

One line: &x on a String is the address of the three-word header on the stack, never of the text on the heap — so a move changes the number you print without relocating a single byte of "hello world".

fn main() {
    let x = String::from("hello world");
    println!("Address: {:p}", &x);
    let y = x;
    println!("Address: {:p}", &y);
}

Two different numbers come out. The obvious reading — "the string moved" — is wrong twice over, and untangling why is worth more than the answer.

There are two addresses, and &x gives you the wrong one

A String is three words on the stack and bytes on the heap: a pointer, a length, a capacity — 24 bytes of header — pointing at a buffer somewhere else entirely.

&x is a &String. It addresses the header. The text lives at x.as_ptr(), which is a different number in a different region of memory, and nothing in the snippet above ever prints it.

So the two lines answer a question about the 24-byte header and say nothing at all about "hello world".

What the move actually did

let y = x; copies those 24 bytes into a new stack slot and marks x dead. The heap buffer is untouched — not copied, not freed, not moved. Only the responsibility for freeing it changed hands, which is what a move is.

Run it with both addresses on show and the split is unmistakable:

One run on one machine — every number here is different on yours
before the move
  &x        (header, on the stack) = 0x7ff7bac5a7f8
  x.as_ptr()(bytes,  on the heap)  = 0x7f8ea6f05f90
after  let y = x;
  &y        (header, on the stack) = 0x7ff7bac5a860
  y.as_ptr()(bytes,  on the heap)  = 0x7f8ea6f05f90

The stack address changed. The heap address is identical — same allocation, same bytes, never touched. Note also how far apart the two regions are: 0x7ff7… is the stack near the top of the address space, 0x7f8e… is the heap, and that gap is visible in every run even though the digits are not.

A changed address proves nothing anyway

The reason the inference fails is more general than String. Give a Copy type the same treatment:

let a: i32 = 5;
let b = a;      // a copy — `a` is still perfectly alive

&a and &b are different addresses too, and here nothing moved: both values exist, both are usable. Two names, two slots, two addresses. That is all a differing address ever tells you — it is a fact about storage, not about ownership.

Why no example in this library prints one

Zero of the repo's examples use {:p}, and that is deliberate. An address is not reproducible: the run above gave 0x7ff7b221a8b8 and 0x7ff7b7d5e8b8 for the same line on two consecutive runs, because the OS randomizes the layout on every launch. Every page here is backed by a recorded answer key that CI re-checks, so an example printing an address would fail the build for a reason that has nothing to do with the lesson.

The technique that survives is to print derived facts instead — a comparison, or a distance:

let cells: [u8; 4] = [10, 20, 30, 40];
let step = &cells[1] as *const u8 as usize - &cells[0] as *const u8 as usize;   // 1, always

That is a real claim about memory (u8 values sit one byte apart), it is checkable, and it is the same on every machine. The example below is written entirely that way — no address is printed anywhere in it, which is why it can have an answer key at all.

The verified output

Verified output of what_an_address_shows.rs — regenerated by tools/run_examples.py, never hand-typed.

1. A String is in two places at once
   size_of::<String>() = 24   ptr + len + capacity, on the stack
   x.len()             = 11   bytes of text, on the heap
   `&x` is the address of the first one. Never the second.

2. What `let y = x;` did
   header at a new address? true
   heap bytes relocated?    false
   same allocation?         true
   24 bytes were copied between stack slots; 11 bytes of text stayed put.

3. A changed address proves nothing about moving — Copy does it too
   a and b at different addresses? true
   and yet a is still alive: a = 5, b = 5

4. What you can safely print: distances, not addresses
   &cells[1] - &cells[0] = 1 byte   — the same on every run
   the addresses themselves are different on every run.

See also

Po polsku

&x na zmiennej typu String daje adres trzysłowowego nagłówka na stosie, a nigdy adres tekstu na stercie. To rozróżnienie decyduje o tym, jak czytać popularne demonstracje „udowadniające” przenoszenie własności przez wypisanie adresu.

Po przeniesieniu adres się zmienia — i to nie dlatego, że przeniósł się choćby jeden bajt napisu "hello world". Przeniósł się nagłówek: skopiowano trzy słowa w nowe miejsce na stosie, a stara nazwa przestała obowiązywać. Bufor ze znakami leży dokładnie tam, gdzie leżał.

Co gorsza, zmieniony adres i tak niczego nie dowodzi. Rozmieszczenie zmiennych na stosie to decyzja kompilatora, wolno mu ją zmienić między kompilacjami, a przy włączonej optymalizacji obie wersje programu często zwracają ten sam adres albo nie mają adresu w ogóle. Dowód oparty na adresie jest dowodem opartym na szczególe implementacyjnym.

Dlatego żaden przykład w tej bibliotece nie wypisuje adresu jako argumentu w sporze. Tam, gdzie trzeba pokazać, że dwie nazwy odnoszą się do różnych miejsc, robi to kontroler pożyczeń — i to jest dowód, który nie zależy od tego, jak akurat skompilował się program.

Szukaj po polsku: adres zmiennej Rust · reprezentacja String w pamięci · rust String memory layout · {:p} formatowanie wskaźnika