CppPointers vs References

Pointers vs References

Pointers and references both let you refer to another variable's memory without copying it, and beginners often confuse them. They solve overlapping problems but make very different trade-offs. This chapter puts them side by side so you can pick the right tool with confidence.

Side-by-side comparison

Aspect

Reference (T&)

Pointer (T*)

Can be null

No — always refers to a real object

Yes — can be nullptr

Can be reseated

No — bound permanently at initialization

Yes — can be reassigned anytime

Must be initialized

Yes, at declaration

No (but should be — else it is wild)

Syntax to access value

Same as the original variable (no operator)

Requires dereferencing with *

Supports arithmetic

No

Yes (pointer arithmetic)

Typical use case

Required function parameters/return values

Optional values, arrays, reseatable handles

When to choose each in modern C++
A useful mental model: reach for a reference when a parameter or return value is required and will never need to be “nothing” or reassigned. Reach for a pointer when the value might legitimately be absent (null), or when you need to reseat it to point somewhere else later. In modern C++, many cases that once called for a raw pointer are now better served by std::optional (an optional value that clearly signals “might not be present”) or smart pointers like std::unique_ptr/std::shared_ptr (covered a few chapters ahead), which manage ownership and lifetime automatically instead of leaving it to the programmer.
The same function, written two ways

Here is one function that requires a valid object, written first with a reference parameter, then with a pointer parameter that has to defensively check for null.

reference_version.cpp

CPP
#include <iostream>
#include <string>

// Reference version: the caller MUST pass a valid std::string.
// There is no way to pass "nothing", so no null check is needed.
void printUpper(const std::string& text) {
    for (char c : text) {
        std::cout << static_cast<char>(std::toupper(c));
    }
    std::cout << std::endl;
}

int main() {
    std::string message = "hello";
    printUpper(message);
    // printUpper(nullptr);   // won't even compile — good!
    return 0;
}

pointer_version.cpp

CPP
#include <iostream>
#include <string>

// Pointer version: the caller CAN pass nullptr, so we must check for it
// every single time before dereferencing.
void printUpper(const std::string* text) {
    if (text == nullptr) {
        std::cout << "(no text provided)" << std::endl;
        return;
    }
    for (char c : *text) {
        std::cout << static_cast<char>(std::toupper(c));
    }
    std::cout << std::endl;
}

int main() {
    std::string message = "hello";
    printUpper(&message);
    printUpper(nullptr);   // compiles fine — must be handled at runtime
    return 0;
}
  • The reference version is shorter, and the compiler guarantees the argument is valid — no null check needed.

  • The pointer version is more flexible (it can represent "no value"), but pushes the responsibility of checking for null onto every caller and every use.

  • If "might be absent" is really part of your function's contract, prefer expressing that with std::optional<T> or a pointer — but if a valid value is always required, a reference communicates that guarantee directly in the type system.

Rule of thumb
Default to references for function parameters. Reach for a pointer (or better, std::optional or a smart pointer) only when “nothing here” or “reassign this later” is a genuine, intentional part of what the code needs to express.