5.2.2 · D4C++ Programming

Exercises — References — lvalue references, difference from pointers

2,582 words12 min readBack to topic

Before we start, one shared picture to anchor the whole page: an lvalue reference is a second label glued to one box, while a pointer is its own box holding an address written on a sticky note.

Figure — References — lvalue references, difference from pointers

Keep that image in mind — most wrong answers below come from forgetting that a reference is not a separate box.


Level 1 — Recognition

Here you only need to recognise whether code is legal and what the rules say. No tracing yet.

For each line, say whether it compiles. Assume int x = 10; already exists above each.

(a) int& r = x;
(b) int& r;
(c) int& r = 5;
(d) const int& r = 5;
(e) int& r = x; r = 20;
Recall Solution L1.1
  • (a) Legal. r becomes another name for x (initialized at declaration ✔).
  • (b) Illegal. A reference must be initialized at declaration — there is no "empty alias".
  • (c) Illegal. 5 is a temporary (an rvalue) — it has no address to alias. A non-const lvalue reference cannot bind a temporary.
  • (d) Legal. A const reference may bind a temporary and extends its lifetime to r's scope.
  • (e) Legal. After binding, r = 20 is not a rebind — it writes 20 into x's box through the alias. x becomes 20.

L1.2 — Match the property

Which of {reference int&, pointer int*} can: (a) be null, (b) be reseated, (c) live uninitialized, (d) do arithmetic, (e) auto-dereference when used?

Recall Solution L1.2
  • (a) null → pointer only (nullptr).
  • (b) reseated → pointer only.
  • (c) uninitialized → pointer only (int* p; is legal though dangerous).
  • (d) arithmetic → pointer only (p + 1).
  • (e) auto-dereference on use → reference only (r acts like the object; a pointer needs *p).

Level 2 — Application

Now trace the code and produce the printed output.

L2.1 — What prints?

int x = 5;
int& r = x;
r = 99;
std::cout << x << " " << r << " " << (&r == &x);
Recall Solution L2.1
  • int& r = x; glues r to x's box.
  • r = 99; writes 99 into that one box.
  • So x reads 99, r reads 99.
  • &r == &x is true, printed as 1 (there is no second address).

Output: 99 99 1

L2.2 — Two-step aliasing

int a = 3, b = 7;
int& r = a;
r = b;          // careful: is this a rebind?
b = 100;
std::cout << a << " " << b << " " << r;
Recall Solution L2.2
  • int& r = a;r aliases a.
  • r = b; is not a rebind (references can never rebind). It reads b's value (7) and writes it into a's box. Now a == 7, and r still aliases a.
  • b = 100; changes only b's box — r and a are untouched.
  • So a == 7, b == 100, r == 7.

Output: 7 100 7

L2.3 — swap via reference

Given swap(int& a, int& b){ int t=a; a=b; b=t; } and int x=1, y=2; swap(x, y);, what are x and y?

Recall Solution L2.3

Inside swap, a aliases x, b aliases y. t = at = 1. a = bx = 2. b = ty = 1.

Result: x == 2, y == 1. See Pass-by-value vs Pass-by-reference.


Level 3 — Analysis

Here you explain why or find the bug.

L3.1 — Spot the dangling reference

int& make() {
    int v = 42;
    return v;
}
int main() {
    int& r = make();
    std::cout << r;   // what happens?
}

Is this safe? Explain in terms of the box picture.

Recall Solution L3.1

Unsafe — undefined behaviour (dangling reference).

  • v is a local box that lives only while make() runs.
  • Returning v by reference hands back a label to a box that is destroyed the instant make() returns.
  • r is now an alias to a dead box; reading r reads freed stack memory. Fix: only return references to objects that outlive the call — members, parameters passed in, or statics. See Dangling references and lifetime.
Figure — References — lvalue references, difference from pointers

L3.2 — Why does one line compile and the other not?

std::string& a = std::string("hi");   // (a)
const std::string& b = std::string("hi"); // (b)
Recall Solution L3.2
  • std::string("hi") is a temporary (an rvalue) — it has no persistent named storage.
  • (a) A non-const lvalue reference cannot bind a temporary (you might modify something about to vanish, and there's no stable box to alias). Illegal.
  • (b) A const reference may bind a temporary and extends its lifetime to b's scope, so the string stays alive as long as b does. Legal. See const correctness.

L3.3 — sizeof reasoning

For int x = 10; int& r = x; int* p = &x; on a 64-bit machine, predict sizeof(x), sizeof(r), sizeof(p).

Recall Solution L3.3
  • sizeof(x) == 4 (a typical int).
  • sizeof(r) == 4sizeof on a reference reports the size of the referred-to object, not of any hidden pointer. A reference is not a separate object.
  • sizeof(p) == 8 — a pointer is its own box holding a 64-bit address. See sizeof and object identity.

Level 4 — Synthesis

Combine several rules to produce or fix code.

L4.1 — Rewrite pointer-swap as reference-swap

Given the C-style version below, rewrite it using references and give the new call site.

void swap(int* a, int* b){ int t=*a; *a=*b; *b=t; }
swap(&x, &y);
Recall Solution L4.1
void swap(int& a, int& b){ int t = a; a = b; b = t; }
swap(x, y);   // no & at the call, no * inside

The references alias x and y directly, so the ugly &/* bookkeeping disappears while the effect is identical (x and y swap).

L4.2 — Design an efficient read-only printer

Write the signature of a function that prints a std::string without copying it and without permitting modification, and explain each keyword. Then show it accepts a literal.

Recall Solution L4.2
void print(const std::string& s);
print("hello");   // legal
  • & → pass by reference, so no copy of a possibly-large string is made.
  • const → promise not to modify the caller's string; also allows binding a temporary.
  • "hello" is a temporary std::string; a const reference may bind it (its lifetime is extended for the call). A non-const std::string& would reject the literal.

L4.3 — Fix the dangling return

Rewrite int& make() from L3.1 so it is safe, keeping the return-by-reference style. State when your fix is valid.

Recall Solution L4.3

One valid fix — reference a static (outlives the call):

int& make() {
    static int v = 42;   // lives for the whole program
    return v;            // safe: v never dies
}

Another valid pattern is to reference a parameter passed in by reference:

int& choose(int& lo, int& hi, bool pickHi){ return pickHi ? hi : lo; }

The rule: only return a reference to something that outlives the call — a member, a passed-in reference, or a static. Returning a reference to a plain local is always wrong.


Level 5 — Mastery

Deep integration — subtle behaviour and full reasoning.

L5.1 — Alias-of-alias

int x = 1;
int& r = x;
int& s = r;   // what does s alias?
s = 8;
std::cout << x << " " << r << " " << s
          << " " << (&s == &x);
Recall Solution L5.1
  • int& s = r; binds s to what r refers to, i.e. to x's box (references are transparent — there is no "reference to a reference" object). So s, r, x are three labels on one box.
  • s = 8; writes 8 into that box.
  • All three read 8; &s == &x is true → 1.

Output: 8 8 8 1

L5.2 — const-ref temporary lifetime, counted

int f() { return 7; }
int main() {
    const int& r = f();   // binds the returned temporary
    int y = r + 3;
    std::cout << r << " " << y;
}

Is r valid on the second line? What prints?

Recall Solution L5.2
  • f() returns the value 7 as a temporary.
  • const int& r = f(); binds that temporary and extends its lifetime to the end of r's scope (the whole main). So r is perfectly valid afterward.
  • y = r + 3 = 7 + 3 = 10.

Output: 7 10 (Contrast L3.1: there the temporary was a local named object returned by reference, whose lifetime is not extended — that dangles. Lifetime extension only applies to a temporary bound directly to a const reference.)

L5.3 — Reference member and identity

struct Wrap { int& ref; };
int a = 5, b = 9;
Wrap w{a};
w.ref = b;      // rebind or write?
std::cout << a << " " << b;
Recall Solution L5.3
  • Wrap w{a}; binds the member ref to a at construction — a reference member must be initialized in the initializer list / aggregate init, and can never be rebound afterward.
  • w.ref = b; is therefore not a rebind. It reads b (9) and writes it into a's box. Now a == 9.
  • b is untouched → b == 9.

Output: 9 9 (Because ref cannot be rebound, a class with a reference member has no default copy-assignment — a subtle real-world consequence.)

L5.4 — Full contrast summary

In one sentence each, state the semantic difference for: (a) &-of, (b) nullability, (c) rebinding, (d) sizeof, between int& and int* const.

Recall Solution L5.4
  • (a) &-of: for a reference, &r == &x (original's address); for int* const p, &p is the pointer's own address.
  • (b) nullability: a reference can never be null; a const pointer can hold nullptr (the const only fixes which address, not that it's valid).
  • (c) rebinding: neither can be reseated — this is the one property they share.
  • (d) sizeof: sizeof(reference) == sizeof(referent); sizeof(int* const) == 8 (its own box). See Rvalue references and move semantics for how T&& extends this family.

Active recall

Recall Rapid fire (cover answers)

Does r = b; after int& r = a; rebind r? ::: No — it writes b's value into a's box. sizeof(r) for int& r = x;? ::: Same as sizeof(x) (4), not a pointer size. Can int& bind std::string("hi")? ::: No; only const std::string& can (and extends its lifetime). Return-by-reference to a static local — safe? ::: Yes, the static outlives the call. int& s = r; where r is a reference — what does s alias? ::: The same object r refers to (references are transparent).


Connections