Skip to content
C++

References vs Pointers

References and pointers both give you indirect access to a variable — a way to work with a value without copying it. But they present very different interfaces and come with very different constraints. A reference is an alias: another name for an object that already exists. A pointer is a variable that stores a memory address. Under the hood the compiler often implements references as pointers, but the surface rules are not the same, and those rules guide you toward the right choice in every situation.

The reference — a permanent alias

Declare a reference with & between the type and the name, and you must initialize it immediately with the variable it will alias. After that, every use of the reference is exactly the same as using the original variable — there is no dereference operator, no arrow, no special syntax. The reference and the original share the same storage; writing through one immediately affects the other.

double balance = 1500.0;
double& ref = balance;   // ref is an alias for balance

ref = 1400.0;            // modifies balance directly
std::cout << balance;    // prints 1400

// A reference cannot be rebound after initialization:
double other = 0.0;
ref = other;  // this ASSIGNS other's value to balance — does NOT rebind ref to other

Three constraints define a reference: it must be initialized when declared (there is no such thing as an uninitialized reference), it can never be null, and it cannot be rebound to a different object after initialization. These constraints are not limitations — they are guarantees. Code that receives a reference parameter knows the object is valid and will not change out from under it.

References shine in function parameters. When you write void f(std::string& s), the caller passes an existing string and the function can modify it without copying. When you write void f(const std::string& s), the function promises not to modify it — you get the efficiency of pass-by-pointer with the convenience and safety of pass-by-value.

The pointer — an explicit address you carry around

A pointer is a variable whose value is a memory address. You declare one by placing * after the type name, obtain an address with the & operator, and read or write the pointed-to value with the * dereference operator. Unlike a reference, a pointer is a first-class variable: it can be uninitialized, set to nullptr, and reassigned to point at a different object at any time.

double balance = 1500.0;
double* p = &balance;    // p holds the address of balance

*p = 1400.0;             // dereference: modifies balance
std::cout << balance;    // prints 1400

// Pointers can be null — use nullptr to express "no object":
double* q = nullptr;
if (q != nullptr) {
    std::cout << *q;     // safe: only dereference when non-null
}

// Pointers can be rebound to a different object:
double other = 0.0;
p = &other;              // p now points to other, not balance

The extra flexibility comes with extra responsibility. Dereferencing a null pointer, dereferencing an uninitialized pointer, or dereferencing a pointer after the object it points to has been destroyed all cause undefined behavior — typically a crash. Every time you use a raw pointer you must reason about whether it is still valid.

Side-by-side syntax

The same six operations look different depending on whether you are using a reference or a pointer. References hide all the address machinery; pointers expose it:

OperationReferencePointer
Declare & bindint& r = x;int* p = &x;
Read the valuer*p
Write the valuer = 5;*p = 5;
Get the address&r (same as &x)p
Rebind to another objectnot possiblep = &y;
Express "no object"not possiblep = nullptr;

Under the hood — references are implemented as pointers

Despite the syntactic difference, the C++ compiler typically implements reference parameters as pointers internally. The book Big C++ Late Objects (Horstmann) illustrates this with a bank withdrawal function. These two implementations are semantically equivalent — the compiler generates nearly identical machine code for both:

// --- Reference version ---
void withdraw(double& balance, double amount)
{
    if (balance >= amount)
        balance = balance - amount;   // reads and writes through the alias
}
withdraw(harrys_checking, 1000);      // caller passes the variable directly


// --- Pointer version (equivalent behind the scenes) ---
void withdraw(double* balance, double amount)
{
    if (*balance >= amount)
        *balance = *balance - amount; // explicit dereference required everywhere
}
withdraw(&harrys_checking, 1000);     // caller must take the address explicitly

The reference version is less noisy — no * scattered through the body, no & at every call site — but the generated binary is typically identical. The reference syntax is essentially syntactic sugar: the compiler handles the address-taking and dereferencing for you, and adds the guarantee that the alias is never null and is never rebound.

When to choose a reference, when to choose a pointer

The cleaner syntax of references makes them the default choice in most situations. Reach for a pointer only when you actually need one of its extra capabilities — nullability or rebinding — or when you are interfacing with C-style APIs that traffic in raw addresses.

Prefer a reference when…

  • →The object always exists — a null case is not meaningful (most function parameters)
  • →You want clean value syntax in the function body without * everywhere
  • →You are iterating with range-for: for (const auto& item : collection)
  • →You are aliasing a long expression locally: auto& cfg = app.config().network();

Prefer a pointer when…

  • →Null is a meaningful state: the parameter is optional, or the result may not exist
  • →You need to rebind the variable to point at different objects over its lifetime
  • →You are working with C APIs that pass or return raw addresses
  • →You are using smart pointers (unique_ptr / shared_ptr) for ownership — they wrap a raw pointer
// Reference parameter — null is impossible, no * noise:
void print_invoice(const Order& order);

// Pointer parameter — nullptr signals "no logging destination":
void process(Order& order, Logger* log = nullptr) {
    if (log) log->write("processing order");
    // ...
}

// Range-for with reference — no copy, no dereference:
for (const std::string& name : names)
    std::cout << name << '\n';

// Rebinding — only possible with a pointer:
double* target = &a;
if (use_b) target = &b;   // now target points at b
*target += 10;

const references — the most common idiom

The most frequent use of references in everyday C++ is the const T& function parameter. It avoids copying a potentially large object (a string, a vector, a struct) while guaranteeing the function will not modify it. It also accepts temporaries and literals, which plain non-const references do not:

void greet(const std::string& name) {
    std::cout << "Hello, " << name << '\n';
}

std::string user = "Alice";
greet(user);          // passes existing string — no copy
greet("Bob");         // passes a temporary string literal — also works
// greet would reject "Bob" if the parameter were std::string& (non-const ref)

// Mutable ref — the function CAN modify the caller's variable:
void make_uppercase(std::string& s) {
    for (char& c : s) c = std::toupper(c);
}
make_uppercase(user); // user is now "ALICE"

The rule of thumb: pass by const T& when the function only reads the argument; pass by T& when it intentionally writes back to the caller; pass by value when the function needs its own copy anyway.

Key rules to remember

References must be initialized at declaration and cannot be null

There is no such thing as int& r; without an initializer. This is a guarantee, not a limitation — callers of a reference parameter know the object is valid.

References cannot be rebound — assignment through a reference modifies the aliased object

Writing ref = other copies the value of other into the aliased variable. It does NOT make ref refer to other. This surprises beginners; keep it in mind.

Pointers can be null — always check before dereferencing

Dereferencing nullptr is undefined behavior and almost always crashes. Test with if (p != nullptr) or simply if (p) before using *p.

The compiler translates reference parameters to pointers internally

References and pointers generate nearly identical machine code. The reference syntax is cleaner and safer because the compiler handles address-taking and dereferencing for you.

Default to const T& for large read-only parameters

Passing a string, vector, or struct by const reference avoids a potentially expensive copy without giving the function the ability to modify the caller's variable.

Use a pointer when null is meaningful or rebinding is needed

If a parameter is optional (may not exist), a pointer lets callers pass nullptr. If you need to point at different objects during the pointer's lifetime, a reference cannot help.

Sign in to track progress