References and pointers
C++ retains C’s pointer model in full and adds references — type-aliased lvalues that, once bound, cannot be rebound — as a distinct kind of indirection. Pointers and references coexist throughout C++ code; the choice between them is governed by ownership and nullability rather than by performance. C++11 introduced rvalue references, which underlie move semantics, and the smart-pointer types that are the conventional choice for owning indirection in modern code. Together, the four kinds — raw pointer, lvalue reference, rvalue reference, and smart pointer — form the surface that distinguishes idiomatic C++ from C.
Raw pointers
A raw pointer is a value that holds the address of an object or of a function. The mechanics — &x to obtain the address, *p to dereference, p->m to access a member through a pointer, pointer arithmetic in units of the pointed-to type — are inherited unchanged from C’s treatment. The differences in C++ are conventional rather than mechanical: raw pointers in modern C++ are non-owning by convention, smart pointers carry ownership, and the choice of which to use is the principal design decision in interfaces that produce or consume objects.
int x = 42;
int *p = &x;
*p = 99; // x is now 99
C++11 introduced nullptr as a typed null-pointer constant of type std::nullptr_t. It supersedes the C-style NULL and the bare 0, both of which had ambiguities that affected overload resolution:
void f(int);
void f(char *);
f(NULL); // ambiguous on platforms where NULL is 0 (calls f(int))
f(nullptr); // unambiguous: calls f(char *)
The convention is to use nullptr exclusively in modern C++.
Lvalue references
An lvalue reference is an alias for an existing object. Once bound, it cannot be rebound:
int x = 42;
int y = 99;
int &r = x; // r is bound to x
r = 0; // assigns 0 to x; x is now 0
r = y; // assigns y to r (and thus to x); does NOT rebind r
// x is now 99; r still aliases x
The differences from a pointer:
- A reference must be initialised on declaration; there is no uninitialised reference.
- A reference cannot be null; the standard provides no mechanism for binding a reference to nothing.
- A reference cannot be reassigned to refer to a different object; assignment to the reference assigns to the bound object.
- The syntax does not require
*to access the bound object; the reference is the object. - The address-of operator on a reference returns the address of the bound object;
&ris&x, not the address of the reference itself (which is not a separately addressable entity).
References are the conventional choice for function parameters that are not modified and do not need to express absence:
void print(const std::string &s); // does not modify s; cannot be null
void modify(int &out); // writes through out
The combination const T& is the idiomatic read-only parameter form: it admits binding to lvalues, rvalues, and any object convertible to T, without copying.
Rvalue references
C++11 introduced rvalue references, written with &&. An rvalue reference binds to a temporary or to an object explicitly cast to rvalue with std::move:
int a = 1;
int &&rr = 42; // OK: rvalue reference to a temporary
int &&rr2 = a; // error: a is an lvalue
int &&rr3 = std::move(a); // OK: explicit cast to rvalue
The principal use of rvalue references is in declarations of move constructors and move assignment operators: a constructor that takes a T&& parameter can transfer the resources of the source rather than copying them. The full treatment is in Move semantics.
A second use is forwarding references — when the && appears in a deduced template parameter (T&& where T is the template parameter), it binds to either lvalue or rvalue as appropriate, preserving value category through std::forward. This is the mechanism by which generic factories perfectly forward their arguments to constructors.
Smart pointers
The standard library provides three smart-pointer class templates in <memory>. They are owning wrappers around raw pointers that automate the corresponding delete:
| Template | Ownership | Notes |
|---|---|---|
std::unique_ptr<T> | Sole ownership | Cannot be copied; can be moved. The default choice. |
std::shared_ptr<T> | Shared ownership | Reference-counted. Last shared_ptr to go out of scope deletes. |
std::weak_ptr<T> | Non-owning observer | Holds a weak reference to a shared_ptr-managed object; converts to shared_ptr for access. |
std::unique_ptr
A unique_ptr owns at most one heap-allocated object and deletes it when the unique_ptr is destroyed:
#include <memory>
auto p = std::make_unique<Widget>(arg1, arg2);
p->method();
// no explicit delete: Widget is destroyed when p goes out of scope
std::make_unique<T>(args...) is the conventional construction primitive: it forwards args to T’s constructor, allocates, and returns a unique_ptr<T>. The combination is exception-safe: if the T constructor throws, no leak occurs because the allocation is owned by the unique_ptr from the moment of construction.
unique_ptr cannot be copied; the copy constructor is deleted. It can be moved:
auto p1 = std::make_unique<Widget>();
auto p2 = std::move(p1); // p1 is now nullptr; p2 owns the Widget
unique_ptr is the default choice for owning indirection. It carries no overhead beyond the raw pointer itself; the delete is invoked at scope exit through ordinary destructor mechanics.
std::shared_ptr
A shared_ptr owns an object jointly with any number of other shared_ptrs; the object is destroyed when the last reference goes out of scope:
auto p1 = std::make_shared<Widget>();
auto p2 = p1; // p1 and p2 both own
auto p3 = p1; // three owners
p2.reset(); // two owners
The reference count is maintained in a control block allocated alongside the object (when constructed via make_shared) or as a separate allocation (when constructed from a raw pointer). The reference count is updated atomically; sharing a shared_ptr across threads is safe.
shared_ptr carries overhead — the control block, the atomic increment and decrement on each copy — and should be used only when shared ownership is genuinely required. Most cases are better served by unique_ptr (sole owner) or by raw pointers and references (non-owning).
std::weak_ptr
A weak_ptr holds a non-owning observer of a shared_ptr-managed object:
auto p = std::make_shared<Widget>();
std::weak_ptr<Widget> w = p;
if (auto locked = w.lock()) { // returns shared_ptr<Widget> or empty
locked->method();
}
The principal use is to break ownership cycles: in a graph of shared_ptrs where two objects each own a shared_ptr to the other, neither’s reference count can drop to zero, and the objects leak. Replacing one direction with weak_ptr resolves the cycle.
Pointer arithmetic
Pointer arithmetic in C++ is C’s: adding n to a T* advances by n * sizeof(T); subtracting two T* into the same array yields a std::ptrdiff_t. The well-defined uses are within a single array (or one past the end). The full mechanics are in C’s pointers page.
The conventional contemporary advice in C++ is to avoid raw pointer arithmetic in user code: prefer iterators, ranges, indexed access on containers, and std::span (C++20) for explicit pointer-and-length pairs:
#include <span>
void print_all(std::span<const int> values) {
for (int v : values) std::cout << v << ' ';
}
int arr[] = {1, 2, 3, 4, 5};
print_all(arr); // span deduced from the array
std::vector<int> v = {6, 7, 8};
print_all(v); // span deduced from the vector
std::span is a non-owning view over a contiguous range; it is to arrays what std::string_view is to strings.
Function pointers and member function pointers
A function pointer holds the address of a free function:
int square(int x) { return x * x; }
int (*fp)(int) = square;
int n = fp(5); // 25
using IntFunc = int(*)(int); // typedef alias
IntFunc fp2 = square;
A member function pointer is a separate kind:
struct Widget {
void method(int);
};
void (Widget::*mfp)(int) = &Widget::method;
Widget w;
Widget *pw = &w;
(w.*mfp)(42); // call through object
(pw->*mfp)(42); // call through pointer-to-object
The construction is rare in routine code; member function pointers are principally used in generic helpers (std::bind, std::mem_fn, std::invoke):
#include <functional>
auto bound = std::bind(&Widget::method, &w, 42);
bound(); // equivalent to w.method(42)
In modern code, lambdas usually replace the explicit member-pointer mechanics:
auto bound = [&] { w.method(42); };
bound();
The strict aliasing rule
C++ inherits and tightens C’s strict aliasing rule: an object’s stored value may be accessed only through an lvalue of certain compatible types. Reinterpreting the bytes of one type as another requires careful tooling:
std::bit_cast<T>(value)(C++20) — the modern, type-safe,constexpr-friendly form for fixed-size, trivially-copyable types.std::memcpy— for runtime byte-by-byte copies between types of compatible storage size.reinterpret_cast— almost always undefined; rarely the right answer.
#include <bit>
#include <cstdint>
float u32_to_float(std::uint32_t bits) {
return std::bit_cast<float>(bits);
}
The conventional rule: when bit_cast works, use it; when it does not (variable-size, non-trivially-copyable types), use memcpy.
Common defects
The recurring pointer-related defects in C++:
| Defect | Description |
|---|---|
| Dangling reference | A reference to an object whose lifetime has ended. Use is undefined; the compiler may diagnose for some patterns. |
| Returning a reference to a local | A function returns T& to a local variable; the lifetime ends with the function. |
string_view outliving its source | A string_view referring to data that has been freed. |
| Smart-pointer cycles | Two objects each holding a shared_ptr to the other; neither is destroyed. Use weak_ptr on one direction. |
Mixing unique_ptr and raw delete | delete-ing a raw pointer to which a unique_ptr already refers, or vice versa. The unique_ptr will delete again. |
new[] / delete mismatch | Allocating with new[] and freeing with delete, or vice versa. |
| Non-virtual destructor | Deleting a derived object through a base pointer when the base has a non-virtual destructor. The derived destructor does not run. |
The discipline in modern C++ is to use smart pointers for ownership, references for non-null non-owning references, raw pointers only for non-owning, possibly-null observation, and to let the type system carry the ownership story whenever possible. The combination — augmented by sanitisers (-fsanitize=address, the equivalent in Clang and GCC) — catches a large fraction of the remaining defects at runtime when they cannot be prevented at compile time.