Pointers and references compared
Functions and library types12 min readLesson 3 of 5
3.1 Two ways to reach an object
A reference gives an existing object a second name. Once a reference is bound, every use of it is a use of the original object. A pointer is a separate object that holds the address of another object; reading or writing the target requires the * operator.
Both mechanisms avoid copying, and they serve different contracts. A reference says that an object must exist and will stay in place for the reference's lifetime. A pointer can be empty, can be redirected to another object, and can be tested before use. Many C++ interfaces use references for required arguments and pointers for optional ones.
#include <iostream>
int main() {
int score = 40;
int& bonus = score;
int* ptr = &score;
bonus += 2;
*ptr += 3;
std::cout << "Score: " << score << "\n";
std::cout << "Bonus: " << bonus << "\n";
return 0;
}Output
Score: 45 Bonus: 45
3.1.1 Binding rules
A reference must be initialized when it is declared, and it cannot be rebound afterward. int& r = score; is valid. int& r; and int& r = nullptr; are not. After r = other; the value of score changes; r still names score.
A pointer can be initialized to nullptr, left uninitialized (a dangerous choice), and pointed at a different object later. Dereferencing a null pointer or an uninitialized pointer is undefined behavior, so test a pointer before reading through it when it may not be set. Both references and pointers can dangle once the object they name is destroyed, and the compiler will not always warn you.
#include <iostream>
#include <string>
int main() {
std::string first = "first";
std::string second = "second";
std::string* pick = &first;
pick = &second;
std::cout << "Picked: " << *pick << "\n";
pick = nullptr;
if (pick != nullptr) {
std::cout << "Value: " << *pick << "\n";
} else {
std::cout << "Nothing selected.\n";
}
return 0;
}Output
Picked: second Nothing selected.
3.1.2 Const with pointers and references
The position of const changes what is protected. const int* p is a pointer to a const int: *p cannot be assigned, and p can point elsewhere. int* const p is a const pointer: p cannot change, and *p can. Reading the declaration from right to left keeps the meaning clear.
References follow a simpler rule. const int& r binds to a value the code will only read, and the compiler rejects assignment through r. A plain int& cannot bind to a literal or a temporary, because changing a temporary would be pointless. A const reference can bind to a temporary, and the temporary lives until the end of the full expression.
#include <iostream>
int main() {
int low = 10;
int high = 20;
const int* view = &low;
int* const anchor = &high;
view = &high;
*anchor = 25;
std::cout << "View: " << *view << "\n";
std::cout << "High: " << high << "\n";
return 0;
}Output
View: 25 High: 25
3.1.3 References in loops
A range-based for loop copies each element into its loop variable unless the variable is a reference. for (std::string name : names) copies every string. for (std::string& name : names) binds to each element and can modify it. for (const std::string& name : names) reads without copying and blocks modification.
The same choice appears with auto. auto drops the reference and copies; auto& keeps a reference; const auto& reads. For small integers the copy costs little, and for strings or structures the reference is usually the better default.
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> names = {"Ada", "Ravi", "Mei"};
for (std::string& name : names) {
name += "!";
}
for (const std::string& name : names) {
std::cout << name << "\n";
}
return 0;
}Output
Ada! Ravi! Mei!
3.1.4 Lifetime and dangling references
A reference or pointer is only usable while its target is alive. If a function returns a reference to a local variable, that variable is destroyed when the function returns, and the caller holds a dangling reference. The same problem appears when you keep a reference into a std::vector and then add enough elements to force reallocation.
References stored as class members must be initialized in the constructor, and the referenced object must outlive the class object. When those conditions are hard to guarantee, store a value or a smart pointer instead. Compilers rarely diagnose a dangling reference, so the lifetime rule has to come from your design.
3.1.5 Choosing between them
Use a reference when the argument is required, the object exists, and the function should work with the caller's object. Use a const reference for read-only access to a large object. Use a pointer when the argument can be omitted, when the code needs to walk an array, or when an older C API requires one.
Return a reference only when the returned object already exists and will outlive the call: a member, a container element, or a static object. Return by value when the function creates something new. A reference parameter also removes the need for a null check at the call site.
A function that returns a reference to a container element depends on the element existing. Check for an empty container before the call, and keep the container alive while the reference is in use.
#include <cstddef>
#include <iostream>
#include <vector>
int& highest(std::vector<int>& values) {
std::size_t best = 0;
for (std::size_t i = 1; i < values.size(); ++i) {
if (values[i] > values[best]) {
best = i;
}
}
return values[best];
}
int main() {
std::vector<int> scores = {72, 91, 64};
highest(scores) += 5;
std::cout << "Top score: " << scores[1] << "\n";
return 0;
}Output
Top score: 96
3.1.6 Practice
- Bind a reference to an integer, change the value through the reference, and print both names.
- Point a pointer at two different integers in turn and print the target after each assignment.
- Write a function that takes a vector by const reference and returns the largest value.
- Modify a vector through a range-based loop, then print it with a const reference loop.
- Explain why
int& r = 5;is rejected whileconst int& r = 5;compiles.
