Lecture File
slides/10_refs.pdf
C++ Core
References as aliases, reference rules, pass-by-reference, const references, returning references, and stream operators.
A C++ reference is an alias: another name for an existing object. References let C++ express mutation and efficient parameter passing without making callers explicitly pass addresses.
The lecture builds three core ideas:
These ideas explain cin >> n, range loops by reference, overloaded I/O operators, and many class interfaces later in the course.
int x = 5;
int &y = x;
x = 3; // y now reads as 3
y = 7; // x now reads as 7
There is one int object. x and y are two names for it. In lecture_code/cpp/refs/simple.cc, printing &x and &y shows the same address.
Do not read int &y = x; as "y stores x's address." A reference may or may not occupy storage as a separate object. The language model is aliasing.
&The ampersand has different meanings depending on context:
int &r = x; // & is part of the type: lvalue reference to int
int *p = &x; // & is an expression operator: address of x
Exam habit: first decide whether you are reading a declaration/type or an expression.
int &r; // illegal
A reference cannot be an alias to nothing.
int x = 1;
int y = 2;
int &r = x;
r = y;
After this, r still aliases x; the value of y was copied into x. This is different from changing a pointer:
int *p = &x;
p = &y; // p now points at y
int x = 1;
int y = 2;
int &a = x; // ok
int &b = 3; // illegal
int &c = x + y; // illegal
3 and x + y are temporary values. A non-const lvalue reference could mutate its referent, so C++ requires it to bind to a real object with stable storage.
const int &r = x + y; // ok
The const promise says the reference will not be used to mutate the temporary. This is why read-only parameters are often written as const T &.
References:
* at use sitesPointers:
The lecture also notes:
Example:
int *p = &x;
int *&rp = p; // rp aliases the pointer variable p
Changing rp changes which address p stores.
Pass-by-value copies:
void times2(int n) {
n *= 2;
}
The caller's variable is unchanged.
Pass-by-reference aliases the caller's variable:
void times2(int &n) {
n *= 2;
}
int x = 4;
times2(x); // x becomes 8
No &x appears at the call site. The function signature controls whether the argument is copied or aliased.
void f(int &a, int b) {
a += 10;
b += 10;
}
int x = 1;
int y = 2;
f(x, y);
Trace:
a aliases x.b is a copy of y.a += 10 changes x to 11.b += 10 changes only the local copy.x == 11 and y == 2.Use T & when the function should mutate the argument:
void readVec(std::istream &in, Vec3D &v);
Use const T & when the function should avoid copying but not mutate:
void printVec(const Vec3D &v);
Use pass-by-value for small cheap values or when the function needs its own copy:
int square(int x);
Lecture rule of thumb: for data types larger than a pointer, pass by reference; if you do not intend to mutate, make it const.
constvoid printNTimes(int &x, int n);
printNTimes(3, 4); // illegal
3 is a temporary. A mutable reference cannot bind to it.
void printNTimes(const int &x, int n);
printNTimes(3, 4); // ok
Now the function promises not to modify x, so binding a temporary is safe.
This also affects expressions:
int x = 2;
int y = 5;
printNTimes(x + y, 3); // ok only with const int &
lecture_code/cpp/refs/conditional_init.cc:
int x = 3;
int y = 5;
int z = 0;
cin >> z;
int &r = z < 0 ? x : y;
r = 22;
If z < 0, r aliases x. Otherwise r aliases y. The conditional expression chooses which existing lvalue the reference binds to.
Trace:
-1: r aliases x, so output is x: 22, y: 5.1: r aliases y, so output is x: 3, y: 22.Once initialized, r cannot later switch to the other variable.
Returning by reference means the function call expression can itself be an alias.
Lecture example from lecture_code/cpp/refs/larger.cc:
int &larger(int &a, int &b) {
return a > b ? a : b;
}
a and b are references to caller variables, so returning one of them is safe. The caller variables outlive the function call.
int x = 10;
int y = 5;
int l = larger(x, y);
--l;
l is a new int copy. Decrementing l does not change x or y.
int &l = larger(x, y);
--l;
Now l aliases whichever variable was larger at that call.
You can also mutate through the returned reference directly:
--larger(x, y);
The expression larger(x, y) refers to either x or y.
Never return a reference to a local variable:
int &bad() {
int x = 5;
return x; // dangling
}
x is destroyed when bad returns. The caller receives an alias to dead stack storage. This is the same lifetime error as returning a pointer to a local variable.
Safe return-by-reference targets:
Unsafe targets:
cin >> n mutates n without the caller writing &n because the extraction operator takes the target by reference.
For a user-defined type:
std::istream &operator>>(std::istream &in, Vec3D &v) {
return in >> v.x >> v.y >> v.z;
}
Reasoning:
in is a reference because streams should not be copied and the operator must update stream state.v is a mutable reference because reading should modify the object.std::istream & so chaining works.Output:
std::ostream &operator<<(std::ostream &out, const Vec3D &v) {
return out << "[" << v.x << ", " << v.y << ", " << v.z << "]";
}
Reasoning:
out is a reference because output mutates stream state.v is const Vec3D & because printing should not change the vector and copying may be expensive.out enables cout << v << endl.for (int x : v) {
x += 1;
}
This modifies only the local copy x.
for (int &x : v) {
x += 1;
}
This modifies the actual elements.
for (const int &x : v) {
cout << x << endl;
}
This avoids copying and prevents mutation.
For:
int x = 5;
int &y = x;
the best mental model is:
storage cell A contains 5
x names storage cell A
y also names storage cell A
After:
y = 7;
there is still one integer object:
storage cell A contains 7
x reads as 7
y reads as 7
This is why lecture_code/cpp/refs/simple.cc prints the same address for x and y.
int x = 1;
int y = 2;
int *p = &x;
int &r = x;
After:
p = &y;
p now stores the address of y. Pointers are objects whose stored address can change.
After:
r = y;
r still aliases x; the value of y is copied into x. References are not reseated by assignment.
void step(int &largest, int &other) {
--largest;
other += 2;
}
int a = 5;
int b = 1;
step(a, b);
step(b, a);
First call:
largest aliases aother aliases ba becomes 4b becomes 3Second call:
largest aliases bother aliases ab becomes 2a becomes 6Reference parameters bind for each call. The parameter names do not permanently belong to a particular caller variable.
larger as an assignable expressionIn lecture_code/cpp/refs/larger.cc:
int &larger(int &a, int &b) {
return a > b ? a : b;
}
Starting with x = 10, y = 5:
cout << larger(x, y)-- << endl;
does not decrement a temporary number. It decrements the caller variable selected by the returned reference.
Trace:
x > y, the function returns an alias to x-- prints the old x and then decrements xx > y becomes false, the conditional returns b, which aliases yyIf the caller writes:
int l = larger(x, y);
the alias is copied into a new integer. If the caller writes:
int &l = larger(x, y);
the alias is preserved.
Unsafe:
int &bad() {
int x = 10;
return x;
}
x dies when the function returns.
Still unsafe:
const int &badConst() {
int x = 10;
return x;
}
const prevents mutation through the reference; it does not extend the lifetime of a local variable after the function returns.
Possibly safe:
int &first(int *arr) {
return arr[0];
}
This returns an alias into caller-owned storage. It is safe only as long as the caller's array remains alive.
Design-risky:
int &Rect::getWidth();
This may be lifetime-safe while the Rect lives, but it can still be a bad interface because it lets clients bypass invariant checks.
For:
istream &operator>>(istream &in, Vec3D &v) {
return in >> v.x >> v.y >> v.z;
}
and input:
1 2 nope
the trace is:
v.x becomes 1v.y becomes 2v.z failsv may be partially updatedThe lecture version is good for learning chaining and reference parameters. A more defensive production version might read into local temporaries and assign to v only if all three reads succeed.
This is legal:
const int &r = 3 + 4;
The temporary is lifetime-extended for the local reference binding.
This is not safe:
const int &bad() {
return 3 + 4;
}
The caller would receive a reference to a temporary that does not live as a stable caller-owned object. For returned references, always identify the storage that will still exist after the function returns.
const on read-only reference parameters, which prevents literals and temporaries from being passed.auto x in a range loop when auto &x is required.int *&p with an illegal pointer-to-reference.For reference binding:
For mutation:
For returned references:
int &r = x;: r aliases x.const int &r = 3;: read-only reference to a temporary is allowed.int &r = 3;: illegal.r = y;: assigns to the object aliased by r.T &: mutable alias parameter.const T &: read-only alias parameter, avoids copies.T & only when the referred-to object stays alive.Built from summaries/10_refs.md and reviewed against slides/10_refs.pdf plus matching files in lecture_code/.