At first glance, int& ref and int* ptr look almost identical — they both let you refer to another object indirectly. Many new C++ developers treat them as interchangeable. They are not.
References and pointers have fundamentally different semantics, different safety guarantees, and different performance characteristics. Choosing the wrong one is one of the most common sources of subtle bugs in C++ codebases.
In this article we’ll compare the two side-by-side, show real code examples, and give you clear rules for when to use each in modern C++ (C++11 through C++23).
1. Quick Syntax Overview
#include <iostream>
int main() {
int value = 42;
int& ref = value; // reference — must be initialized
int* ptr = &value; // pointer — can be nullptr
std::cout << ref << '\n'; // 42
std::cout << *ptr << '\n'; // 42
ref = 100; // changes value
*ptr = 200; // also changes value
std::cout << value << '\n'; // 200
}
Notice how the syntax differs when you use them:
- With a reference you use the variable name directly (
ref = 100). - With a pointer you must dereference it (
*ptr = 200).
2. The Big Differences at a Glance
| Feature | Reference (T&) |
Pointer (T*) |
Winner for most code |
|---|---|---|---|
| Must be initialized | ✅ Yes (at declaration) | ❌ Can be nullptr |
Reference |
| Can be null | ❌ Impossible | ✅ Yes | Pointer (when you need optionality) |
| Can be rebound / reseated | ❌ No — it always refers to the same object | ✅ Yes — ptr = other; |
Reference (safer) |
| Null-safety guarantee | ✅ Compiler enforces it | ❌ You must check manually | Reference |
| Syntax when using | Clean — no * |
Explicit — requires * |
Reference |
| Can be stored in containers | ❌ No (references are not objects) | ✅ Yes | Pointer or std::reference_wrapper |
| Performance | Zero overhead (usually optimized away) | Zero overhead | Tie |
3. Nullability & Lifetime Guarantees
References cannot be null. This is one of their greatest strengths.
void print(const std::string& s) { // good
std::cout << s << '\n';
}
// ❌ Bad style
void print(const std::string* s) {
if (s) std::cout << *s << '\n';
}
Because a reference must be initialized, you also get stronger lifetime guarantees. The compiler will refuse to compile code that would create a dangling reference in obvious cases.
4. Rebinding Rules — “References Can’t Be Reseated”
int a = 10;
int b = 20;
int& ref = a; // ref is now permanently bound to a
ref = b; // This assigns 20 to a, it does NOT rebind ref!
int* ptr = &a;
ptr = &b; // This actually changes what ptr points to
This behavior surprises many beginners. A reference is like an alias — once created, it is glued to its target for its entire lifetime.
5. const-correctness
Both support const, but the meaning is slightly different:
const int& cref = value; // reference to const int
int const& cref2 = value; // same thing (preferred style)
const int* cptr = &value; // pointer to const int
int* const cptr2 = &value; // const pointer (pointer itself cannot change)
Modern C++ almost always prefers const T& for read-only function parameters.
6. When You Need Reference Semantics but Also Copyability
References cannot be stored in containers or copied. Enter std::reference_wrapper (C++11):
#include <functional>
#include <vector>
std::vector<std::reference_wrapper<int>> vec;
int x = 5, y = 10;
vec.push_back(x); // works!
vec.push_back(y);
for (auto& r : vec) {
r.get() += 100; // modifies original x and y
}
7. When You MUST Use a Pointer
Even in modern C++, there are still legitimate uses for raw pointers:
- Optional parameters (
std::optionalis often better now, but pointers are still common) - Interfacing with C libraries
- Dynamic polymorphism with base-class pointers (though
std::unique_ptr/std::shared_ptrare preferred for ownership) - Implementing data structures that need explicit null nodes
- Low-level memory management (rare in application code)
Use smart pointers for ownership.
Use raw pointers only when you need a nullable non-owning handle.
8. Modern C++ Best Practices (2026 edition)
- Pass by
const T&for read-only parameters (almost always). - Pass by
T&only when the function must modify the argument. - Return by value (thanks to move semantics and RVO).
- Use
std::string_viewinstead ofconst std::string&when you just need to read a string. - Use
std::span<T>instead of raw array pointers. - Never use raw
new/delete— always smart pointers.
Result: cleaner, safer, and more maintainable code.