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:
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:
&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¶
- Ownership and moves — what changed hands, and the three rules behind it
- The anatomy of a
String— the three words this page is addressing - A name is not a place — the same distinction from the other side: a binding is not a storage location
- Meet the byte — the unit that distance was measured in
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