A reference in C++ is an alias: a second name for an object that already exists. Unlike a pointer, it cannot be reseated, it cannot be null, and it must be initialised at the moment it is declared. Those restrictions are what make references the safer of the two, and they are why a reference is the right alias whenever you simply want to work with an existing object.
A reference is a second name
A reference is declared with an & and initialised with the object it refers to. From then on the two names are the same object.
#include
int main() {
int duration = 180;
int& alias = duration;
std::cout << "duration: " << duration << '\n';
std::cout << "alias: " << alias << '\n';
alias = 240;
std::cout << "after write through alias: " << duration << '\n';
duration = 300;
std::cout << "after write through original: " << alias << '\n';
return 0;
}The output shows the two names tracking each other perfectly:
duration: 180
alias: 180
after write through alias: 240
after write through original: 300There is no indirection to think about, no * to remember, and no way for the alias to point somewhere else later.
References cannot be reseated
Once a reference is bound, it stays bound. Attempting to change what it refers to does not compile.
#include
void assign_through_reference(int& target) {
target = 999;
}
int main() {
int first = 1;
int second = 2;
int& alias = first;
std::cout << "alias sees: " << alias << '\n';
std::cout << "second is: " << second << '\n';
// alias = second; // does not compile: a reference cannot be reseated
assign_through_reference(alias);
std::cout << "after function: " << first << '\n';
return 0;
}The output shows that writing through the alias reached the original object:
alias sees: 1
second is: 2
after function: 999This is a deliberate design decision. A reference that could be reseated would behave unpredictably for anyone reading the function, and the safety of "it always refers to that one object" is worth more than the flexibility.
References cannot be null
A reference must be bound to an actual object when it is declared. There is no such thing as a null reference.
#include
int main() {
int value = 42;
int& alias = value;
// int& missing; // does not compile: no default construction
std::cout << "value: " << value << '\n';
std::cout << "alias: " << alias << '\n';
return 0;
}The output confirms the alias is usable from the moment it is declared:
value: 42
alias: 42When you genuinely need "no object", you want a pointer and nullptr, not a reference.
References in function parameters
A by-reference parameter is how a function modifies the caller's object. A const reference parameter is how a function reads a large object without copying it.
#include
#include
struct Track {
std::string title;
int seconds;
};
void describe(const Track& track) {
std::cout << track.title << " runs " << track.seconds << " seconds\n";
}
void set_duration(Track& track, int seconds) {
track.seconds = seconds;
}
int main() {
Track track = {"Slow Tide", 243};
describe(track);
set_duration(track, 300);
describe(track);
return 0;
}The output shows the read-only call and then the modifying call:
Slow Tide runs 243 seconds
Slow Tide runs 300 secondsThe first parameter is const Track&, so the function may read every field but cannot change any of them. The compiler enforces that, which means a caller can trust that describe has no side effects.
References in range-based for loops
The range-based for loop can bind each element either by value or by reference, and the choice decides whether you can modify the original collection.
#include
#include
#include
int main() {
std::vector tracks = {"one", "two", "three"};
for (std::string& track : tracks) {
track += " (edited)";
}
for (const std::string& track : tracks) {
std::cout << track << '\n';
}
return 0;
}The output shows the edits landed in the original vector:
one (edited)
two (edited)
three (edited)Binding by const reference in the second loop avoids a copy per element while making the read-only intent obvious.
Choosing between a reference and a pointer
- Reference — the object always exists and the function always works with that one object. The default for function parameters.
- Const reference — the object always exists but the function must not change it. The default for large read-only parameters.
- Pointer — the object might not exist, or the function might need to point at different objects over time.
- Pointer to const — the object might not exist and must not be changed through the pointer.
References as return values
A function may return a reference, and that is often the most efficient way to hand back a large object without copying it. The rule is the same as for parameters: the referenced object must outlive the call.
#include
#include
struct Playlist {
std::string name;
int tracks;
};
const std::string& longest_name(const Playlist& playlist) {
static const std::string empty = "";
if (playlist.name.empty()) {
return empty;
}
return playlist.name;
}
int main() {
Playlist playlist = {"Late Night Sessions", 42};
std::cout << "name: " << longest_name(playlist) << '\n';
return 0;
}The output shows the reference being used directly without a copy:
name: Late Night SessionsThe static local exists so that the function always has something valid to return even when the caller passes an empty playlist. Returning a reference to a temporary would be a dangling reference in exactly the way the previous lesson described.
Quick reference
- A reference is an alias for an existing object; it cannot be null and cannot be reseated.
- A reference must be initialised at its declaration.
- Writing through a reference changes the original object.
- A by-reference parameter modifies the caller's object; a const reference reads it.
- In a range-based
for,auto&allows modification andconst auto&does not. - Use a pointer only when "no object" or "a different object later" is part of the design.
Try it yourself: write a function that takes a [[std::string&]] and uppercases it in place, then a version that takes a [[const std::string&]] and prints it. Call the first with a mutable string and confirm the change sticks. Then try to write to the second function's parameter and read the compiler's complaint, which is a precise description of const correctness.