References
Reviewed & published by Brayan K
A reference is a second name for an existing variable, written with &. References let functions modify the caller's data, avoid expensive copies with const&, and underpin the swap idiom. This lesson makes references click — and shows the dangling-reference trap to avoid.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Create a reference as an alias with int& r = x
- Pass by reference so a function can modify the caller's variable
- Write a swap function using references
- Pass big objects by const reference to avoid copies
- Use auto& and const auto& in range-based for loops
- Avoid dangling references to destroyed objects
💡 Real-World Analogy
A reference is a nickname. If everyone in the office calls "Elizabeth" by "Liz", then "Liz" and "Elizabeth" are the same person — message either name and the same person hears it. You can't later decide that "Liz" now means a different colleague (a reference can't be rebound), and a nickname only makes sense once the person exists (a reference must be initialized). Passing by reference is like giving a colleague your real phone, not a photocopy of it — anything they change, you see when it comes back.
1. A Reference Is an Alias
Declare a reference with type& name = existing;. From then on the reference and the original are the same object — change one and the other changes too. References must be initialized when created and can never be made to refer to a different object afterward.
#include <iostream>
using namespace std;
int main() {
int score = 10;
// A reference is a second NAME for the same variable.
int& alias = score; // 'alias' and 'score' are now the same box.
alias = 25; // changing alias changes score
cout << "score: " << score << '\n'; // score: 25
score = 40; // and vice versa
cout << "alias: " << alias << '\n'; // alias: 40
// A reference MUST be initialized and can never be rebound.
// int& bad; // ERROR: references must be initialized
return 0;
}
// ✅ Expected output:
// score: 25
// alias: 402. Pass by Value vs Pass by Reference
By default C++ copies arguments, so a function works on its own copy and the caller's variable is untouched. Take a parameter by reference (int& n) and the function operates on the caller's actual variable — changes persist after the call returns.
#include <iostream>
using namespace std;
// Pass by value: works on a COPY, caller is unchanged.
void tryAddByValue(int n) { n += 100; }
// Pass by reference: works on the caller's actual variable.
void addByRef(int& n) { n += 100; }
int main() {
int x = 5;
tryAddByValue(x);
cout << "after value: " << x << '\n'; // after value: 5 (unchanged)
addByRef(x);
cout << "after ref: " << x << '\n'; // after ref: 105
return 0;
}
// ✅ Expected output:
// after value: 5
// after ref: 1053. The Swap Idiom
Swapping two variables is the textbook reason references exist. Because both parameters are references, the function exchanges the caller's actual values — something impossible with pass-by-value. (The standard library's std::swap does exactly this for you.)
#include <iostream>
using namespace std;
// Classic swap — only possible because both params are references.
void swap2(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
int main() {
int x = 1, y = 9;
swap2(x, y);
cout << "x=" << x << " y=" << y << '\n'; // x=9 y=1
return 0;
}
// ✅ Expected output:
// x=9 y=14. const References: Read Without Copying
For large objects like strings and vectors, copying them into a function is wasteful. A const reference (const string& s) gives the function direct, read-only access — no copy, and the compiler guarantees the function can't modify your data. This is the standard way to pass big objects you only need to read.
#include <iostream>
#include <string>
using namespace std;
// const reference: read a big object WITHOUT copying it,
// and promise not to modify it.
size_t describe(const string& text) {
cout << "text: " << text << '\n';
// text += "!"; // ERROR: text is const, can't modify
return text.size();
}
int main() {
string title = "Effective Modern C++";
cout << "length: " << describe(title) << '\n';
// text: Effective Modern C++
// length: 20
return 0;
}
// ✅ Expected output:
// length: text: Effective Modern C++
// 205. References in Range-Based Loops
In a range-based for, the loop variable is a copy by default. Use auto& to modify each element in place, and const auto& to read each element efficiently without copying. Choosing the right one is a small habit that pays off in correctness and speed.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3, 4};
// auto& lets us MODIFY each element in place.
for (auto& n : nums) n *= 10;
// const auto& reads without copying or modifying.
for (const auto& n : nums) cout << n << ' ';
cout << '\n'; // 10 20 30 40
return 0;
}
// ✅ Expected output:
// 10 20 30 40Your turn. Make this function actually change the caller's variable:
#include <iostream>
using namespace std;
// 🎯 YOUR TURN — make this function actually double the value.
void doubleIt(___) { // 👉 take the parameter by reference: int& n
n *= 2;
}
int main() {
int value = 21;
doubleIt(value);
cout << value << '\n'; // ✅ Expected: 42
return 0;
}🧩 Reorder Challenge
These lines should increment a variable through a reference parameter and print the new value. Reorder so the program prints 6.
A. bump(score);
B. void bump(int& n) { n += 1; }
C. int score = 5;
D. cout << score << '\n';
E. int main() {
F. }B. void bump(int& n) { n += 1; }
E. int main() {
C. int score = 5;
A. bump(score);
D. cout << score << '\n';
F. }Why: bump must be defined before main calls it. Inside main you declare score = 5, then pass it to bump. Because the parameter is int&, n += 1 modifies the caller's score directly, so printing afterward shows 6.
🧠 Quick Recall
Predict the output before revealing each answer.
int a = 3; int& r = a; r = 7; cout << a;
7 — r is an alias for a, so assigning through r changes a.
void f(int n){ n = 99; } int x = 1; f(x); cout << x;
1 — n is a by-value copy, so the change inside f doesn't affect x.
vector<int> v = {1,2}; for (auto& e : v) e += 5; cout << v[0] << v[1];
67 — auto& modifies each element in place, so the vector becomes {6, 7}.
Pro Tips
- 💡 Default to const& for big read-only params: strings, vectors, and custom classes.
- 💡 Use plain reference to "return" extra results via out-parameters, like a swap or min/max pair.
- 💡 Prefer const auto& in loops when you only read elements — no copies, no accidental edits.
- 💡 Never return a reference to a local: it dangles the moment the function returns.
Common Errors (and the fix)
- Uninitialized reference: int& r; won't compile — a reference must bind to something immediately.
- Expecting by-value to mutate: if a function doesn't change your variable, you forgot the & on the parameter.
- Dangling reference: returning a reference to a local variable is undefined behavior. Return by value or reference a longer-lived object.
- Copying in a loop: for (auto e : bigVec) copies every element. Use const auto& to read efficiently.
📋 Quick Reference
| Syntax | Meaning | Note |
|---|---|---|
| int& r = x; | reference (alias) to x | r and x are the same object |
| int& r; | uninitialized reference | error — must bind at creation |
| void f(int n) | pass by value | works on a copy; caller unchanged |
| void f(int& n) | pass by reference | edits the caller's variable |
| void f(const string& s) | pass by const reference | read big objects with no copy |
| void swap(int& a, int& b) | out-parameters | return extra results via references |
| for (auto x : v) | loop over copies | edits to x don't affect v |
| for (auto& x : v) | loop by reference | modify each element in place |
| for (const auto& x : v) | read-only loop | best default — no copy, no edits |
| return localRef; | dangling reference | undefined behavior — never return a local ref |
| std::swap(a, b); | library swap | uses references under the hood |
Mini-Challenge: Min and Max
Write a function that returns two results through reference out-parameters. Match the expected output in the comments.
#include <iostream>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: min and max in one call
// 1. Write a function void minMax(int a, int b, int& lo, int& hi)
// that sets lo to the smaller and hi to the larger of a, b.
// (Use references so the caller receives BOTH results.)
// 2. In main, call it with 8 and 3, then print "min=3 max=8".
//
// Expected output: min=3 max=8
// your code here
return 0;
}🎉 Lesson Complete
- ✅ A reference (int& r = x) is an alias — changes flow both ways
- ✅ Pass by reference to let a function modify the caller's variable
- ✅ Swap and other multi-result functions rely on references
- ✅ const& reads big objects without copying
- ✅ auto& modifies loop elements; const auto& reads them cheaply
- ✅ Next: a checkpoint that combines everything you've learned so far
Practice quiz
How do you declare a reference named r that aliases an existing int x?
- int& r = x;
- int* r = x;
- int r = &x;
- ref int r = x;
Answer: int& r = x;. A reference is written type& name = existing;, so int& r = x; makes r another name for x.
What does this print? int a = 3; int& r = a; r = 7; cout << a;
- 3
- 7
- 0
- It does not compile
Answer: 7. r is an alias for a, so assigning 7 through r changes a itself. It prints 7.
What does this print? void f(int n){ n = 99; } int x = 1; f(x); cout << x;
- 99
- 0
- 1
- It does not compile
Answer: 1. n is taken by value, so f works on a copy. The caller's x is unchanged and 1 prints.
Which parameter type lets a function modify the caller's variable?
- int n
- const int& n
- int& n
- const int n
Answer: int& n. A plain (non-const) reference int& n aliases the caller's variable, so changes persist after the call.
Why pass a large object as const std::string& s?
- It copies the string so the original is safe
- It reads the object without copying and forbids modifying it
- It allows the function to resize the string
- It is required by the compiler for all strings
Answer: It reads the object without copying and forbids modifying it. A const reference gives direct read-only access with no copy, and the const promises the function won't modify it.
Why must both swap parameters be references in void swap2(int& a, int& b)?
- References are faster to type
- So the function exchanges the caller's actual values, not copies
- Because swap can only take two arguments
- To avoid including <utility>
Answer: So the function exchanges the caller's actual values, not copies. Only by taking references does the function operate on the caller's real variables, making the exchange visible afterward.
What does this print? vector<int> v = {1,2}; for (auto& e : v) e += 5; cout << v[0] << v[1];
- 12
- 67
- 00
- 1626
Answer: 67. auto& binds a reference to each element, so += 5 modifies the vector in place to {6, 7}, printing 67.
In a range-based for loop, which form reads large elements efficiently without copying or modifying them?
- for (auto x : c)
- for (auto& x : c)
- for (const auto& x : c)
- for (int x : c)
Answer: for (const auto& x : c). const auto& binds a read-only reference: no copy is made and the element cannot be changed.
What is a dangling reference?
- A reference that was never initialized
- A reference to an object that has already been destroyed, e.g. returning a reference to a local
- A reference passed by value
- A reference to a const object
Answer: A reference to an object that has already been destroyed, e.g. returning a reference to a local. Returning a reference to a local variable leaves it referring to a destroyed object — using it is undefined behavior.
Why does int& r; fail to compile?
- References cannot be ints
- A reference must be initialized when it is created
- You must use a pointer instead
- References require the const keyword
Answer: A reference must be initialized when it is created. A reference must bind to something at creation; it can never be left uninitialized or rebound later.
Continue this course
- Previous: Namespaces
- Next: Checkpoint: C++ Basics — Combine I/O, strings and enums in a build challenge — then a quiz
- Quick reference: C++ cheat sheet › Memory & Ownership
Frequently asked questions
What is the difference between a reference and a pointer?
A reference is an alias for an existing object: it must be initialized when created, can never be rebound to something else, and is used with normal variable syntax. A pointer is a separate variable holding an address: it can be null, can be reassigned, and needs * to dereference. References are simpler and safer for the common case of 'this function works on that variable'.
When should I pass by const reference?
Pass by const reference (const std::string& s) whenever a function only reads a parameter and the type is larger than a couple of machine words. It avoids copying the whole object while promising not to modify it. For small types like int or double, plain pass-by-value is just as fast and clearer.
Why does my function not change the variable I passed in?
By default C++ passes arguments by value, so the function gets its own copy. To let a function modify the caller's variable, take the parameter by reference: void addOne(int& n). Now n is an alias for the caller's variable and changes are visible after the call.
What is a dangling reference?
A dangling reference refers to an object that has already been destroyed, for example returning a reference to a local variable. Using it is undefined behavior. The rule of thumb: never return a reference to a local; only return references to data that outlives the function, such as a member or a parameter that was passed in by reference.
What does auto& vs auto do in a range-based for loop?
for (auto x : v) copies each element, so changes to x do not affect the container. for (auto& x : v) binds a reference, letting you modify elements in place. for (const auto& x : v) reads each element without copying and without allowing changes — the best default for read-only loops over large elements.