5.2.2 · D5C++ Programming

Question bank — References — lvalue references, difference from pointers

2,602 words12 min readBack to topic

First: the memory model these traps attack

Before any trap makes sense you need the picture in your head. Everything below is just this one image, poked from different angles.

Look at the figure. The reference r draws no new box — it is a tag hanging off x's box, which is why &r reads 0x100 (x's address) and sizeof(r) measures the int, not an address: there is no second box to measure. The pointer p is a box at 0x200; &p is 0x200, sizeof(p) is the size of an address, and *p follows the stored 0x100 back to x. Keep this side-by-side in mind for every "why" on this page. It is the crisp version of what sizeof and object identity formalizes.

In the top row, p = &y; erases 0x100 from p's box and writes 0x200p now points at y. In the bottom row r = y; looks identical but there is no address inside r to overwrite; the line copies y's value into x's box. That single distinction powers half the "Spot the error" items below.

The figure shows the two diagnostics side by side: the reference error is caught by the compiler (a red stop before the program ever runs), while the uninitialized-pointer error slips through compilation and only explodes at runtime. This is why references push a whole class of bugs from "runtime undefined behavior" back to "won't compile".

Read the red bar: without the const-reference binding a temporary dies at the end of its full expression (the short bar); the binding stretches it to the end of r's scope (the long bar). This is the motivating reason const T& parameters can accept literals and function results, and it ties directly into const correctness (the const is what makes the extension safe — you promise not to write) and into Dangling references and lifetime (use r after the bar ends and it dangles).


True or false — justify

Every claim below is a sentence people commonly believe. Decide true/false, then give the why before revealing. Keep the two memory figures above in view.

A reference is a special kind of pointer that C++ hides from you.
False — a pointer is a separate box holding an address (figure s01, 0x200); a reference is not a distinct object at all, just a second name label on the existing box, so there is nothing to "hide".
int& r = x; copies the value of x into r.
False — at declaration the = binds the alias; no new box, no copy. Only a later r = ... writes into the shared box.
After int& r = x;, the expression &r gives the address of the reference itself.
False — &r gives &x (0x100 in s01), because the reference has no box of its own and therefore no separate address to hand out.
A const int& r is exactly the same thing as an int* const p.
False — p is still a real box (s01) holding an address you can inspect and dereference; r is not a box, has no address of its own, and is auto-dereferenced.
You can create a reference now and decide what it aliases later.
False — with no box to hold a "pending" binding, an unbound reference cannot even be represented, so the compiler rejects it at compile time (figure s03).
Two different references can be made to alias the same variable.
True — int x; int& a = x; int& b = x; hangs three name labels off one box. Aliasing one target from many names is fine; what's forbidden is rebinding one name.
Once bound, r = y; makes r start aliasing y instead of x.
False — as the bottom row of s02 shows, that line assigns the value of y into x (the box r names). A reference can never be reseated onto a different box.
sizeof(r) for int& r = x; returns the size of a machine address (like a pointer).
False — with no separate reference box to measure (s01), sizeof reports the size of the referred-to type (sizeof(int)), not an address size.
A const int& parameter is slower than passing int by value.
False (and misleading) — for small types like int, by-value is typically as fast or faster; but for large objects const T& avoids a copy, which is why it exists. See Pass-by-value vs Pass-by-reference.
const int& r = 5; is illegal because 5 has no address.
False — it is legal; the const reference binds a temporary holding 5 and extends its lifetime (the long red bar in s04). Only the non-const int& r = 5; is illegal.

Spot the error

Each snippet has a bug or a misconception. Name it and say why.

int& r; r = x;
A reference must be initialized when declared; int& r; is a compile-time error (s03) because there is no box to bind later. You cannot declare an empty alias and fill it on the next line.
int& r = 5;
A non-const lvalue reference cannot bind an rvalue/temporary — 5 has no persistent box to alias. Use const int& r = 5;, whose const promise makes lifetime extension safe (s04).
int& f() { int x = 5; return x; }
Returns a reference to a local whose box is destroyed when f returns — a dangling reference and undefined behavior. See Dangling references and lifetime.
void inc(int a) { a++; } ... int n = 0; inc(n); and expecting n == 1.
a is passed by value, a copy in its own box; a++ changes the copy only. To modify the caller use int& a so a is a label on the caller's box.
int x = 10; int& r = x; int& r = y; (in the same scope, trying to "reseat").
Redeclaring r in the same scope is an error; and even conceptually you can't reseat a reference (s02 bottom row) — it stays bound to x for life.
void print(std::string& s); print("hi");
A string literal produces a temporary; a non-const std::string& can't bind it. Change the parameter to const std::string&, which may bind temporaries and extend their lifetime for the call.
int* p = nullptr; int& r = *p;
Dereferencing a null pointer to form a reference is undefined behavior — there's no live box to alias. "References can't be null" doesn't protect you if you feed them a bad object.
std::string& s = getName(); where getName() returns std::string by value.
A non-const reference cannot bind the returned temporary; prefer const std::string& s = getName(); (which extends the temporary's life, s04) or just take by value.

Why questions

Why must an lvalue reference be initialized, when a pointer need not be?
A reference is the alias — with no box of its own, an unbound one can't exist, so the compiler rejects it immediately (s03). A pointer is a real box that can validly hold nullptr or garbage until you assign it.
Why can't int& r = 5; compile but const int& r = 5; can?
A non-const reference could be written through, which would mean writing into a temporary literal — meaningless. Making it const promises no writes, so binding the temporary and extending its life (s04) is safe. Ties to const correctness.
Why does &r return the original's address rather than the reference's own?
Because there is no separate reference box (s01); r and x denote the exact same storage, so their addresses are identical. See sizeof and object identity.
Why does sizeof(r) yield sizeof(int) and not the size of an address?
sizeof measures an object, and a reference is not one — there is no address-holding box behind r (unlike the pointer box in s01). So the language defines sizeof on a reference to measure the type it aliases, i.e. the int itself.
Why prefer a reference over a pointer when a function must always receive a valid object?
A reference guarantees non-null and non-rebindable binding, so the callee needn't check for null or worry the target moves — the type system encodes "always one valid object".
Why prefer a pointer over a reference for an optional argument?
A pointer's box can hold nullptr to mean "no object here"; a reference has no box and cannot express absence, so an optional parameter is naturally a pointer (or std::optional). Compare with Pointers in C++.
Why does passing a big object as const std::string& matter but passing int that way rarely does?
The const& avoids copying the whole string's bytes; for an int, a copy is one machine word — cheaper than the indirection a reference introduces.
Why is "a reference is just a const pointer" a seductive but wrong analogy?
Both can't be reseated (s02), so it feels right — but a const pointer is still an inspectable, dereferenceable box with its own address, while a reference has no separate identity at all (s01).
Why does swap(int& a, int& b) need no * inside its body while the pointer version does?
a and b are already name labels on the caller's boxes, so plain a, b read/write them directly. Pointers hold addresses in their own boxes, so you must dereference with * to reach the value.

Edge cases

Can you have a reference to a reference, like int& & rr = r;?
No — C++ forbids declaring references to references directly. (Reference collapsing exists only in templates/typedefs for Rvalue references and move semantics, and always collapses to a single reference.)
Can you make an array of references, int& arr[3];?
No — arrays require elements that are objects with storage and addresses; references aren't objects (no box in s01), so an array of them is ill-formed.
Can a reference alias itself, e.g. int& r = r;?
No meaningful case — r isn't initialized yet at that point, so you're binding to an uninitialized/undefined thing; it's undefined behavior, not a valid alias.
Can you take the address of a reference to store it in a pointer, int* q = &r;?
Yes, but q ends up holding &x (s01), not some private reference address — there isn't one. q points at the aliased object, exactly as if you'd written &x.
Can you bind a reference to a bit-field, e.g. struct S { int b : 3; }; int& r = s.b;?
No — a bit-field has no addressable byte of its own, and a non-const reference needs a real addressable object to alias. (A const int& r = s.b; is allowed, binding a copy held in a temporary, not the bit-field itself.)
Can you bind a reference to a variable declared register, e.g. register int x; int& r = x;?
Taking the address of a register object was ill-formed historically (and register is deprecated/removed in modern C++); since a reference conceptually needs the object's address, binding to a genuine register-only object doesn't work — the object must live in addressable memory.
What happens to a const int& bound to a temporary once its scope ends?
The temporary's life was stretched to match the reference (long bar, s04); when the reference leaves scope the temporary is destroyed with it. Using the reference after that is a dangling reference — see Dangling references and lifetime.
Is int& r = x; where x is itself a reference (int& x = y;) binding to x or to y?
To y — a reference has no box of its own, so binding to x binds to whatever box x already labels, i.e. y. All three names hang off the one box.
Can a reference bind to a const object non-const, e.g. const int c = 5; int& r = c;?
No — that would let you write through r into a const object. You need const int& r = c;, preserving the promise not to modify. See const correctness.
If p is a valid pointer, is int& r = *p; legal and safe?
Legal and safe only while *p names a live object. The reference now labels that box; if the object is later destroyed, r dangles even though "references can't be null".

Connections