Pointers vs References
Pointers and references both let you refer to another variable's memory without copying it, and beginners often confuse them. They solve overlapping problems but make very different trade-offs. This chapter puts them side by side so you can pick the right tool with confidence.
Side-by-side comparison
Aspect | Reference (T&) | Pointer (T*) |
|---|---|---|
Can be null | No — always refers to a real object | Yes — can be nullptr |
Can be reseated | No — bound permanently at initialization | Yes — can be reassigned anytime |
Must be initialized | Yes, at declaration | No (but should be — else it is wild) |
Syntax to access value | Same as the original variable (no operator) | Requires dereferencing with * |
Supports arithmetic | No | Yes (pointer arithmetic) |
Typical use case | Required function parameters/return values | Optional values, arrays, reseatable handles |
When to choose each in modern C++
The same function, written two ways
Here is one function that requires a valid object, written first with a reference parameter, then with a pointer parameter that has to defensively check for null.
reference_version.cpp
#include <iostream>
#include <string>
// Reference version: the caller MUST pass a valid std::string.
// There is no way to pass "nothing", so no null check is needed.
void printUpper(const std::string& text) {
for (char c : text) {
std::cout << static_cast<char>(std::toupper(c));
}
std::cout << std::endl;
}
int main() {
std::string message = "hello";
printUpper(message);
// printUpper(nullptr); // won't even compile — good!
return 0;
}pointer_version.cpp
#include <iostream>
#include <string>
// Pointer version: the caller CAN pass nullptr, so we must check for it
// every single time before dereferencing.
void printUpper(const std::string* text) {
if (text == nullptr) {
std::cout << "(no text provided)" << std::endl;
return;
}
for (char c : *text) {
std::cout << static_cast<char>(std::toupper(c));
}
std::cout << std::endl;
}
int main() {
std::string message = "hello";
printUpper(&message);
printUpper(nullptr); // compiles fine — must be handled at runtime
return 0;
}The reference version is shorter, and the compiler guarantees the argument is valid — no null check needed.
The pointer version is more flexible (it can represent "no value"), but pushes the responsibility of checking for null onto every caller and every use.
If "might be absent" is really part of your function's contract, prefer expressing that with std::optional<T> or a pointer — but if a valid value is always required, a reference communicates that guarantee directly in the type system.