Polyglot
Languages C++ pointers
C++ § pointers

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; &r is &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:

TemplateOwnershipNotes
std::unique_ptr<T>Sole ownershipCannot be copied; can be moved. The default choice.
std::shared_ptr<T>Shared ownershipReference-counted. Last shared_ptr to go out of scope deletes.
std::weak_ptr<T>Non-owning observerHolds 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++:

DefectDescription
Dangling referenceA reference to an object whose lifetime has ended. Use is undefined; the compiler may diagnose for some patterns.
Returning a reference to a localA function returns T& to a local variable; the lifetime ends with the function.
string_view outliving its sourceA string_view referring to data that has been freed.
Smart-pointer cyclesTwo objects each holding a shared_ptr to the other; neither is destroyed. Use weak_ptr on one direction.
Mixing unique_ptr and raw deletedelete-ing a raw pointer to which a unique_ptr already refers, or vice versa. The unique_ptr will delete again.
new[] / delete mismatchAllocating with new[] and freeing with delete, or vice versa.
Non-virtual destructorDeleting 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.