A reference is another name
#include <iostream>
using namespace std;
int main() {
int marks = 91;
int &alias = marks; // alias IS marks, under a second name
alias = 100;
cout << marks << "\n"; // 100
cout << &marks << "\n"; // same address
cout << &alias << "\n"; // same address
return 0;
}
There is no second variable here. alias and marks are two names for one set of bytes. Nothing extra is stored, nothing is copied, and there is no dereference step to write.
Compare with the pointer version:
#include <iostream>
using namespace std;
int main() {
int marks = 91;
int *p = &marks; // p is a separate variable holding an address
*p = 100; // must dereference to reach marks
cout << marks << "\n"; // 100
return 0;
}
Same result, more syntax, and one extra variable that can be null, can be moved, and can go stale.
The three rules of references
1. A reference must be initialised. int &r; is a compile error. A pointer can sit uninitialised holding garbage — a reference cannot exist without something to refer to.
2. A reference cannot be re-seated. Once bound, it names that variable forever.
#include <iostream>
using namespace std;
int main() {
int a = 1, b = 2;
int &r = a;
r = b; // this does NOT make r refer to b
// it copies b's value INTO a
cout << a << " " << b << "\n"; // 2 2
return 0;
}
This surprises people. r = b looks like reassignment of the reference. It is an assignment through the reference. a is now 2.
3. There is no null reference. A reference always refers to a real object, so you never check it before use. That is the main safety benefit.
Passing to functions
#include <iostream>
using namespace std;
void byValue(int x) { x = 99; }
void byPointer(int *x) { *x = 99; }
void byReference(int &x){ x = 99; }
int main() {
int a = 1, b = 1, c = 1;
byValue(a);
byPointer(&b);
byReference(c);
cout << a << " " << b << " " << c << "\n"; // 1 99 99
return 0;
}
Look at the call sites. byPointer(&b) shouts that something may change. byReference(c) looks identical to byValue(a). That is the one real cost of references: you cannot tell from the call whether the argument gets modified. You have to read the function signature.
const reference: the workhorse
This is the most useful pattern in everyday C++.
#include <iostream>
#include <vector>
using namespace std;
// copies the whole vector on every call: slow
int sumSlow(vector<int> v) {
int s = 0;
for (int x : v) s += x;
return s;
}
// no copy, and const promises not to modify: fast and safe
int sumFast(const vector<int> &v) {
int s = 0;
for (int x : v) s += x;
return s;
}
int main() {
vector<int> big(1000000, 1); // one million ones
cout << sumSlow(big) << "\n"; // 1000000, after copying 4 MB
cout << sumFast(big) << "\n"; // 1000000, copying nothing
return 0;
}
sumSlow copies four megabytes on every call. sumFast passes what is effectively an address. For a vector, string, map or any class, always take a const & unless you need a copy. For an int, double or char, pass by value — copying 4 bytes is cheaper than the indirection.
The rule of thumb: const T& for anything bigger than a pointer that you only read. T& when the function must modify the caller's object. Plain T for small built-in types and when you genuinely want your own copy to modify.
Reference in a range-for loop
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> v = {1, 2, 3};
for (int x : v) x *= 10; // modifies copies: no effect
for (int n : v) cout << n << " ";
cout << "\n"; // 1 2 3
for (int &x : v) x *= 10; // modifies the real elements
for (int n : v) cout << n << " ";
cout << "\n"; // 10 20 30
for (const int &x : v) cout << x << " "; // read only, no copy
cout << "\n";
return 0;
}
That single & is the difference between a loop that works and a loop that silently does nothing. When a "modify every element" loop appears to have no effect, this is why.
Returning a reference
#include <iostream>
#include <vector>
using namespace std;
// SAFE: the vector outlives the call
int &at(vector<int> &v, int i) { return v[i]; }
// UNSAFE: local dies at the closing brace
// int &bad() { int x = 5; return x; }
int main() {
vector<int> v = {1, 2, 3};
at(v, 1) = 50; // assigning to a function call
cout << v[1] << "\n"; // 50
return 0;
}
Returning a reference to a local variable is the same bug as returning the address of a local in C, with the same consequence: a dangling reference to a destroyed stack frame. The compiler warns; listen to it.
When you still need pointers
References cannot do three things, and each is a real use case.
- Be null. A function parameter that is optional needs a pointer, so
nullptrcan mean "not supplied". - Be re-pointed. Walking a linked list needs
node = node->next. A reference cannot move. - Own heap memory.
new,deleteand smart pointers work with pointers.
Everything else — parameters, loop variables, avoiding copies — should use references.
Use nullptr, not NULL, in C++. NULL is the integer 0 in disguise, so with overloaded functions f(NULL) can pick f(int) instead of f(char*). nullptr has its own type and always means "pointer to nothing".
Summary table
| Pointer | Reference | |
|---|---|---|
| Can be null | yes | no |
| Must be initialised | no | yes |
| Can be re-assigned to another object | yes | no |
| Syntax to use | *p |
r |
| Address-of at call site | f(&x) |
f(x) |
Arithmetic (p + 1) |
yes | no |