What you'll learn
Quick Answer
A reference is another name for an existing object. It must be initialised, can never be null, and can never be made to refer to something else. A pointer is a variable holding an address, so it can be null, reassigned and stored in containers. Pass small values such as int and double by value, pass large objects by const reference to avoid copying, and use a pointer when the argument is genuinely optional or when you need to move it along.
The copy you did not ask for
Start with the version of this topic that actually costs marks in a coding round, because it is not a syntax question at all.
#include <iostream>
#include <string>
#include <vector>
void printAll(std::vector<std::string> names) { // copies the whole vector
for (auto n : names) { // copies each string again
std::cout << n << "\n";
}
}This compiles without a warning and prints the right answer. It also copies the entire vector into the function, performing a heap allocation for every string in it, and then copies each string a second time on every iteration of the loop. For a vector of city names read from input, that is two rounds of allocation for work that should have touched nothing.
The corrected version adds nothing but a const and an &, in two places:
void printAll(const std::vector<std::string>& names) {
for (const auto& n : names) {
std::cout << n << "\n";
}
}Now nothing is copied. The function receives a reference to the caller's vector, and each loop iteration binds a reference to an existing string. The const promises you will not modify anything, so a caller can pass a temporary and a reader can see at a glance that this function only observes.
This is the single highest-value habit in C++: by default, pass and loop over anything larger than a machine word by const reference. C++ copies by default because copying is the safe choice, not the cheap one. Python and Java hand you a handle to the object, so the same code there costs nothing, and people arriving from those languages write the slow version without realising a copy happened at all.
What a reference actually is
A reference is an alias. After int& r = a; the names r and a refer to the same object, and there is no way to tell them apart afterwards. That leads directly to the behaviour most beginners get wrong:
int a = 5, b = 9;
int& r = a;
r = b; // this ASSIGNS 9 into a. It does not rebind r.
std::cout << a; // prints 9
std::cout << &r; // the address of a, foreverPeople expect r = b to make r refer to b, the way reassigning a pointer works. It cannot. Once a reference is bound it is bound for its whole life, and every operation on it is an operation on the original object.
Everything else follows from that one rule:
- A reference must be initialised.
int& r;does not compile, because an alias for nothing is meaningless. - A reference cannot be null. There is no
nullptrequivalent, so a function takingint&never needs a check for absence. - There is no reference arithmetic.
++rincrements the value, not an address. - You cannot make an array of references or store references directly in a
std::vector; usestd::reference_wrapperor pointers for that. sizeof(r)reports the size of the referred-to type, not the size of a pointer.
Under the bonnet a reference is usually implemented as a pointer that is dereferenced automatically, but the standard does not require it to occupy any storage at all, and for a simple local alias the compiler normally emits nothing. Treat that as an implementation detail. At the language level a reference is a name, and a pointer is a value.
Pass by value, by reference or by pointer
Three ways to hand data to a function, with a fairly mechanical rule for choosing between them.
By value when the type is small and cheap to copy: int, double, char, an enum, a pointer, or a small struct of a few such members. The function gets its own copy, so the caller's variable is untouched. Passing an int by const reference is usually a small pessimisation, since the reference adds an indirection where the value would have fitted in a register.
By const reference when the object is large and the function only reads it: std::string, std::vector, maps, and your own classes. No copy, and const documents the intent.
By non-const reference when the function must modify the caller's object and that object always exists.
void addBonus(int& marks) { marks += 5; } // must exist
void addBonus(int* marks) { if (marks) *marks += 5; } // may be absent
int main() {
int score = 40;
addBonus(score); // reference version: no visible clue it changes score
addBonus(&score); // pointer version: the & announces it at the call site
return 0; // score is now 50
}By pointer when the argument is genuinely optional, when the function needs to reseat what it points at, or when you are talking to a C API. Some house styles use a pointer for every output parameter precisely so that the & at the call site warns the reader that the variable is about to change. Others prefer non-const references and rely on the function name. Both are defensible; pick one and be consistent within a codebase.
There is a fourth case people miss. If the function is going to keep a copy, take the parameter by value and std::move it into place. That way a caller passing a temporary gets a move instead of a copy, and a caller passing a named object pays one copy either way.
const references, temporaries and dangling
A const reference does something a non-const one cannot: it can bind to a temporary, and binding extends that temporary's life to the life of the reference. That is why void greet(const std::string& name) accepts greet("Anita"), even though the string literal has to be turned into a temporary std::string first. A plain std::string& parameter would reject the same call.
Lifetime extension has limits, and both of them bite in real code. It does not survive being returned, and it does not apply to reference members initialised in a constructor.
const std::string& bad() {
std::string s = "Pune";
return s; // s is destroyed here; the caller gets a dangling ref
}Compilers usually warn about that exact shape. They cannot warn about the more common one, where the reference outlives its target because a container moved underneath it:
std::vector<int> v{1, 2, 3};
int& first = v[0];
v.push_back(4); // may reallocate and free the old buffer
first = 99; // writing to memory the vector no longer ownsNothing here is null, so no null check would have caught it. This is the fundamental difference between a reference and a copy: a reference is only as valid as the object behind it. If you hold one across anything that can destroy, resize or reallocate the owner, you have a bug that will show up as corrupted data long before it shows up as a crash.
The practical rule is that references are for the duration of a call. Take them as parameters, bind them to loop elements, use them and let them go. When you need to store a link to something for later, store a pointer, an index, or a weak_ptr, so that the possibility of the target disappearing is visible in the type.
When a pointer is the right answer
References are the better default for parameters, but there are jobs they cannot do at all.
Optional values. A reference must refer to something, so absence has to be expressed some other way. A pointer with nullptr is the traditional answer; std::optional is the modern one when you are returning a value rather than referring to one.
Walking a structure. A traversal reassigns its cursor on every step, which a reference cannot do:
struct Node { int value; Node* next; };
int length(const Node* head) {
int n = 0;
for (const Node* p = head; p != nullptr; p = p->next) {
++n;
}
return n;
}Try to write that with a reference and you are stuck at the first element, because p = p->next would assign a whole node over the head instead of moving on. Linked lists, tree traversal and iterator-style code all need pointers or iterators for this reason.
Storing links. You cannot put references in a std::vector. Collections of polymorphic objects hold pointers, and in modern code that usually means std::vector<std::unique_ptr<Shape>> so the ownership is explicit.
Talking to C. Every C library function takes pointers, including buffers and out-parameters, so anything below the C++ layer is pointer territory.
The convention that ties it together in modern C++ is about ownership rather than syntax. A unique_ptr or shared_ptr says this code owns the object and will destroy it. A raw pointer says this code is only looking, and might be looking at nothing. A reference says this code is only looking, and there is definitely something there. If you pick the one whose promise matches what your function actually does, readers get the answer without opening the implementation.
