Skip to content
C Codeloom
C++

C++ References vs Pointers Explained

A clear comparison of references and pointers in C++, when each is the right tool, and how their semantics interact with const, ownership, and APIs.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • The semantic differences between references and pointers
  • When references are the cleaner API choice
  • How const interacts with both
  • Rebinding, nullability, and lifetime concerns
  • Conventions used by the standard library

Prerequisites

  • Basic familiarity with C++ syntax and functions

What and Why

C++ has two distinct ways to refer to an object indirectly: pointers and references. They look similar in some contexts but behave very differently. Pointers are first-class values you can reassign and reason about as addresses. References are aliases bound at construction.

Choosing well matters because the choice signals intent. A function that takes T& says “I am operating on your object.” A function that takes T* says “I might also accept nothing.”

Mental Model

A pointer is a variable holding an address. It can be null, reassigned, and arithmetic can be performed on it. A reference is a name for an existing object. It must be bound at declaration, cannot be null, and cannot be reseated to a different object.

int x = 10;
int* p = &x;       // p stores address of x; can be nullptr
int& r = x;        // r is another name for x; bound forever

p = nullptr;        // OK: pointer reassigned
// r = ... is just assignment to x, not rebinding
int y = 20;
p = &y;            // p now points to y
r = y;             // assigns 20 to x (still bound to x)
Pointer vs reference at a glance

Hands-on Example

A function that swaps two integers shows the difference clearly.

#include <iostream>

void swap_ref(int& a, int& b) {
    int tmp = a; a = b; b = tmp;
}

void swap_ptr(int* a, int* b) {
    if (!a || !b) return;       // pointers can be null
    int tmp = *a; *a = *b; *b = tmp;
}

int main() {
    int x = 1, y = 2;
    swap_ref(x, y);              // clean, no & at call site
    swap_ptr(&x, &y);            // explicit address-of
    std::cout << x << ' ' << y << '\n';
}

The reference version reads better because the call site has no extra syntax and the function body cannot accidentally deref a null. The pointer version is appropriate when “no object” is a valid input.

For containers and ranges, references give natural iteration:

std::vector<std::string> names{"Ada", "Bob"};
for (auto& name : names) name += "!";       // modify in place
for (const auto& name : names)
    std::cout << name << '\n';              // read-only view

Common Pitfalls

  • Dangling references: a reference outlives the object it aliases. Returning a reference to a local variable is undefined behavior. The compiler often warns, but not always.
  • Reference members and assignment: a class with a non-const reference member cannot be reassigned by the default operator. Prefer a pointer member if you need rebindable indirection.
  • Confusing T& with rebinding: r = expr always assigns through the reference, never reseats it. Once bound, a reference stays bound for life.
  • Pointer arithmetic on non-array memory: incrementing a pointer that does not point into an array is undefined.
  • Implicit conversions: passing a temporary to a non-const reference parameter is illegal, but to a const reference parameter it extends the temporary’s lifetime. Knowing this prevents surprises with return values.

Practical Tips

For parameters:

  • Pass by const T& to receive a read-only view of any object.
  • Pass by T& to mutate the caller’s object.
  • Pass by T* only when null is meaningful, or when you need pointer arithmetic.
  • Pass by value (T) for cheap types like int, std::string_view, or when you need an owned copy anyway.

For returning:

  • Return by value for new objects; the move and copy-elision rules make this cheap.
  • Return T& or const T& only when the lifetime is guaranteed (member access, container element).
  • Return raw pointers only when null encodes “not found” and the caller does not own the result.

For ownership, do not use raw pointers at all. Use std::unique_ptr or std::shared_ptr. Raw pointers should mean “observe, do not delete.”

For APIs, follow the standard library: std::vector::at returns a reference; std::map::find returns an iterator that might be end(); std::optional<T&> is intentionally not supported, so use T* for optional references in C++ APIs.

Wrap-up

References and pointers solve overlapping problems with different defaults. References give you concise, non-null aliases that read like normal variables. Pointers give you reassignable, nullable handles useful for ownership-free observers and optional parameters. Pick the one whose semantics match your intent, and your code will be both shorter and harder to misuse.