C++ / REFERENCES, POINTERS, AND NULL
References as aliases and their binding rules
Create references as second names for existing objects, know what T& and const T& can bind to, and why assignment never rebinds a reference.
What you will learn
- Initialize every reference at declaration; there is no unbound or reseatable reference.
- Read int& r = x; as "r is another name for x", not "r points to x".
- Bind T& only to modifiable lvalues; use const T& to accept literals and temporaries.
- Recognize that r = y; copies y's value into r's referent instead of rebinding r.
Understanding References as aliases and their binding rules
A reference is not a new object holding something; it is a second name bound to an object that already exists. Because the binding is made by the declaration itself, the initializer is mandatory: int& alias = score; compiles, while int& alias; does not. From that point on, every use of alias behaves as if you had typed score instead: alias = 5 writes into score, &alias yields score's address, and sizeof alias is sizeof(int) rather than the size of some hidden handle.
Which initializers are legal follows directly from what the reference permits. A non-const int& must bind to a modifiable lvalue, meaning a named variable, an array element, *p, or a call returning int&, because writes through the name need a lasting object to land in. A const int& additionally accepts rvalues, so literals, arithmetic results, and functions returning by value all bind, and the temporary produced lives as long as the reference does. That also explains an asymmetry beginners trip over: const double& d = someInt; is fine because the compiler materializes a temporary double, whereas double& d = someInt; is rejected, since writes to d would vanish into that temporary instead of reaching the int.
The main consequence of binding once is that assignment can never move a reference. alias = other means "store other's value in the object alias names", which is why a stray assignment quietly overwrites the original variable instead of aiming the alias elsewhere. There is likewise no null reference and no operation that reports or changes what a reference is bound to; a compiler usually implements one as an address, but the language exposes none of that. When code needs to change targets or express "nothing yet", a reference is the wrong tool.
extra
<iostream>
int main() {
int score = 10;
int& alias = score; // bound here and forever: alias IS score
alias = 25; // the write lands in score
std::cout << score << ' ' << alias << '\n';
int other = 99;
alias = other; // copies 99 into score; does NOT rebind alias
std::cout << score << ' ' << other << '\n';
std::cout << (&alias == &score) << ' ' << (&alias == &other) << '\n';
const int& doubled = score * 2; // a reference to const may bind to a temporary
std::cout << doubled << '\n';
}
A reference is a name permanently bound to one existing object at its declaration, so every operation written on the name is performed on that object.
Worked examples
Aliasing a caller's variable through a parameter
Shows a reference parameter naming the caller's object, and which arguments each reference kind will accept.
<iostream>
void bump(int& n) { n += 1; }
void report(const int& n) { std::cout << "value " << n << '\n'; }
int main() {
int counter = 7;
bump(counter); // n is another name for counter
std::cout << counter << '\n';
report(counter);
report(41 + 1); // const int& binds to the temporary 42
// bump(41 + 1); // error: no lasting object for a writable alias
}
Example explained
Line 1bump takes int& n, so n is another name for counter; n += 1 increments the caller's variable with no copy involved.
Line 2report(counter) prints 8 because the earlier bump already changed the one object both names refer to.
Line 3report(41 + 1) works because const int& may bind to the temporary produced by the addition, and that temporary lives until the call finishes.
Line 4The commented call is rejected: int& promises write access, but 41 + 1 has no persistent object for those writes to reach.
Copy versus alias in a range-based for loop
Contrasts a loop variable that copies each element with one that aliases it, then shows a reference tracking its object rather than a value.
<iostream>
int main() {
int data[3] = {1, 2, 3};
for (int v : data) v *= 10; // v is a fresh copy each iteration
std::cout << data[0] << ' ' << data[1] << ' ' << data[2] << '\n';
for (int& v : data) v *= 10; // v names the element itself
std::cout << data[0] << ' ' << data[1] << ' ' << data[2] << '\n';
int& mid = data[1];
data[1] = 7; // written through the element's own name
std::cout << mid << '\n';
}
Example explained
Line 1for (int v : data) copies each element into v, so v *= 10 changes a copy that dies at the end of the iteration and the array prints unchanged.
Line 2for (int& v : data) makes v an alias for each element in turn, so the multiplication is applied to the array elements themselves.
Line 3mid is bound to data[1] at its declaration; writing 7 through data[1] changes that same object, so mid reads 7 and not the earlier 20.
Line 4A reference never stores its own copy of the value, which is why the two names can never disagree.
Important notes
A reference is not an object in its own right: you cannot form an array of references, and &r and sizeof r report the referent, so the binding itself is unobservable.
Lifetime extension in const int& r = makeValue(); lasts only for r's own scope; it does not follow the reference out of a function, so returning such a reference leaves it dangling.
Common mistakes
Declaring int& r; and planning to attach it later: the declaration itself fails to compile, because a reference with nothing to alias has no meaning.
Reading alias = other as a rebind: instead other's value is copied into the original object, the program still compiles and runs, and the lost data surfaces as a bug much later.
Passing an expression such as x + 1 to a function taking int&: it will not compile, since a non-const lvalue reference cannot bind to a temporary, and switching the parameter to const int& compiles but removes the ability to modify it.
Try it yourself
Change, predict, then run
Declare int a = 3, b = 8; and int& r = a;, then run r = b; r += 1; and print a, b, (&r == &a), and (&r == &b). Write down your prediction for all four values before running it.
Open the C++ workspaceCheck your understanding
Given int x = 1, y = 5; int& r = x; r = y; y = 100; what is the value of x afterwards?
- 5
- 100
- 1, because assigning to a reference has no effect
- It does not compile, because r is already bound to x
Show answer
r names x for its entire life, so r = y copies y's current value, 5, into x; the later y = 100 writes to a different object entirely. Answering 100 assumes r = y reseated r onto y, but assignment through a reference always reaches the referent and never the binding, which is fixed at the declaration.