C++ / REFERENCES, POINTERS, AND NULL
References versus pointers: choosing between them
Decide between a reference and a pointer by asking whether the target can be missing and whether it can change, then encode that answer in your signatures.
What you will learn
- Use a reference for a required, fixed target; a pointer when absent or re-seatable
- Return a pointer from lookups that can fail, a reference from accessors that cannot
- Explain why r = x assigns through a reference instead of re-binding it
- Know that a reference member deletes copy assignment; use a pointer to rebind
Understanding References versus pointers: choosing between them
A reference is not an object: it is a name the compiler attaches to storage that already exists, and the attachment is made once, in the initialization. No operation in the language changes it, because every later appearance of the name means the referred-to object itself, which is why `r = x` copies a value rather than moving the alias. A pointer is an ordinary object with its own storage holding an address, so it can be read, compared, overwritten, and left holding the null value. Every practical difference between the two follows from that one structural fact.
So the choice is settled by two questions about the connection you are modelling: can there legitimately be nothing to refer to, and does the connection ever have to move to a different object after it is made? Two answers of no mean a reference, because the requirement then lives in the type: the callee cannot be handed null and nobody writes a check that can never fire. A yes to either means a pointer, and the null test that comes with it is the price of the flexibility you asked for.
The decision surfaces in two places beginners do not expect. A parameter declared `int&` is invisible at the call site, since `bump(count)` reads like a copy, which is why read-only input is normally passed as `const T&` and some codebases deliberately use a pointer for out-parameters so `bump(&count)` marks the write. A reference data member is fixed for the object's lifetime, so the compiler cannot generate a copy assignment operator for that class, and a reference cannot be an element of a standard container; anything that must be rebindable has to be a pointer or a `std::reference_wrapper`.
<iostream>
<string>
<vector>
struct Account {
std::string id;
int balance;
};
// A lookup can come up empty, so the return type must be able to say "nothing".
Account* findAccount(std::vector<Account>& accounts, const std::string& id) {
for (Account& a : accounts) {
if (a.id == id) {
return &a;
}
}
return nullptr;
}
// Precondition: accounts is not empty, so the result is always an object, never a maybe.
Account& firstAccount(std::vector<Account>& accounts) {
return accounts.front();
}
int main() {
std::vector<Account> accounts{{"alice", 120}, {"bob", 45}};
if (Account* found = findAccount(accounts, "bob")) {
found->balance += 5;
std::cout << found->id << " now has " << found->balance << '\n';
}
if (findAccount(accounts, "carol") == nullptr) {
std::cout << "carol: no account\n";
}
Account& first = firstAccount(accounts); // no null check is possible or needed
first.balance -= 20;
std::cout << accounts[0].id << " now has " << accounts[0].balance << '\n';
}
A reference is a permanent second name for one object and a pointer is a variable holding an address, so the choice depends on whether you must express nothing, or a different target later.
Worked examples
Assignment means two different things
Shows why a reference cannot be re-seated while an identical-looking assignment retargets a pointer.
<iostream>
int main() {
int a = 1;
int b = 2;
int& r = a; // r is a second name for a, permanently
int* p = &a; // p is an object whose value is a's address
r = b; // writes through r: a becomes 2
p = &b; // writes to p itself: p now targets b
std::cout << "a = " << a << ", b = " << b << '\n';
std::cout << "r = " << r << ", *p = " << *p << '\n';
*p = 7; // changes b, leaves a alone
std::cout << "a = " << a << ", b = " << b << '\n';
}
Example explained
Line 1`int& r = a;` fixes the alias at initialization, while `int* p = &a;` merely stores an address in a variable you can reassign.
Line 2`r = b;` has no way to mean re-bind, so it copies b's value into a, which is why a prints as 2.
Line 3`p = &b;` modifies p, not a, so the two assignments on adjacent lines have completely different effects.
Line 4`*p = 7;` writes through p's current target, so only b changes and r still reports a's value.
Reference member versus pointer member
Demonstrates the class-design consequence: a reference member is fixed and blocks copy assignment, a pointer member can be retargeted or detached.
<iostream>
<string>
struct Logger {
std::string name;
};
class RefView {
public:
explicit RefView(Logger& l) : log_(l) {}
void report() const { std::cout << "ref -> " << log_.name << '\n'; }
private:
Logger& log_;
};
class PtrView {
public:
explicit PtrView(Logger* l) : log_(l) {}
void retarget(Logger* l) { log_ = l; }
void report() const {
std::cout << "ptr -> " << (log_ ? log_->name.c_str() : "none") << '\n';
}
private:
Logger* log_;
};
int main() {
Logger file{"file"};
Logger net{"net"};
RefView rv(file);
rv.report();
// rv = RefView(net); // will not compile: copy assignment is deleted
PtrView pv(&file);
pv.report();
pv.retarget(&net);
pv.report();
pv.retarget(nullptr);
pv.report();
}
Example explained
Line 1`Logger& log_` is bound in the constructor's member initializer list and stays bound, which is exactly the guarantee that lets `report()` dereference with no check.
Line 2The commented `rv = RefView(net);` fails because an implicitly generated copy assignment would have to re-bind a reference member, so the compiler deletes it.
Line 3`retarget` is a one-line assignment only because `log_` is an object holding an address rather than an alias.
Line 4`retarget(nullptr)` puts the object into an unattached state, forcing `report()` to branch, which is the ongoing cost of the extra flexibility.
Important notes
`std::vector<int&>` does not compile, because vector elements must be assignable objects; store pointers, indices, or `std::reference_wrapper<int>` when you need a container of aliases.
A raw pointer parameter says nothing about ownership and does not imply the callee will delete anything; use a smart pointer type when ownership actually transfers.
Common mistakes
Trying to re-point a reference with `r = other;`, which quietly copies other's value into the original object, so two variables end up equal and the bug gets misdiagnosed as broken arithmetic.
Writing `int& v = *maybeNull;` without testing the pointer: dereferencing null is undefined behaviour at that line, and the crash usually shows up later at an innocent-looking use of v.
Declaring a pointer parameter for something that must never be null and then sprinkling `if (!p) return;` through the body: callers still pass nullptr, the contract stays unwritten, and a reference parameter would have made the error unwritable.
Try it yourself
Change, predict, then run
Build `std::vector<std::string> names{"ada", "grace"}` and write both `std::string* find(std::vector<std::string>&, const std::string& key)` returning nullptr on failure and `std::string& firstName(std::vector<std::string>&)`. Print what a failed lookup gives you, then attempt to make an existing reference refer to a different element and explain what your code actually did to the vector.
Open the C++ workspaceCheck your understanding
A class needs a member that starts out attached to nothing and can later be pointed at a different object. Which choice fits, and why?
- A pointer member, because it is an object whose value is an address, so it can hold nullptr and be overwritten later
- A reference member initialised in the constructor, because a bound reference can be re-bound by assigning to it
- A `T* const` member, because a const pointer documents that the class controls its target
- A reference member, because references and pointers differ only in the syntax used to reach the object
Show answer
Only a pointer member has a value of its own that can be nullptr and later reassigned. Option 1 is the tempting one, but assigning to a reference writes through it and changes the referred-to object's value instead of the binding, and a reference member also causes the implicit copy assignment operator to be deleted. Option 2 fails for the same reason a reference does: a `T* const` cannot be re-seated after its initialization either.