C++ · Intermediate

Pointers and References in C++

10 min readUpdated September 24, 2026Every example verified

In short: A pointer is a variable holding the address of another object; it can be reassigned, compared and set to nullptr to mean no object. A reference is a second name for an existing object: bound when declared, never rebound, never null. Use references for parameters a function should read or modify without copying, and pointers when the target may be absent or may change.

Two ways to reach an object indirectly

Every object in a running program lives at an address. &target gives that address, and a pointer stores one: int* p = ⌖. Writing *p means the object p points at, so *p += 2 changes target itself. A pointer can later be pointed at a different object, compared with another pointer, or set to nullptr to mean no object at all.

A reference is declared with & in the type, int& alias = target;, and is simply a second name for target. There is nothing to dereference: you use alias exactly like the original. A reference must be bound when declared and can never be rebound; alias = 18 writes 18 into target.

Both exist mainly for function parameters. C++ passes arguments by value, so a function receives a copy and the caller's variable is untouched. A reference parameter, void apply(double& price), makes the parameter an alias for the argument: changes reach the caller and nothing is copied. A pointer parameter does the same job, but the caller must pass an address with &c and the function must cope with nullptr. That obligation is also the pointer's strength: a function can return a pointer to what it found, or nullptr for nothing found. A reference cannot express absence.

When a function only reads a large object such as a std::string, take it by const reference, const std::string& name: no copy, no modification, and literals and temporaries are accepted, which a non-const reference refuses.

Pointers into arrays support arithmetic: if hit points at an element of readings, hit - readings is its index and hit + 1 is the next element, and an array's name converts to a pointer to its first element. Pointers obtained with & or from an array own nothing, so you never delete them; memory from new needs delete, a job modern code usually hands to std::unique_ptr from <memory>. Use the C++11 keyword nullptr for an empty pointer rather than NULL or 0, which are not pointer types and can select the wrong overload.

Syntax

 C++ · syntax
int value = 7;
int* p = &value;        // pointer: holds the address of value
*p = 9;                 // dereference: write through the pointer
p = nullptr;            // no object; must be checked before use

int& r = value;         // reference: another name for value, bound for life
r = 12;                 // writes into value

void bump(int& n);      // parameter by reference: changes the caller's variable
void show(const std::string& s);   // read-only, no copy
int* find(int* first, int count, int wanted);   // may return nullptr

int* p, int *p and int * p are the same declaration. Only one name per declaration gets the pointer type: int* a, b; makes a a pointer and b a plain int.

Reading and writing through a pointer and a reference

A thermostat setting reached three ways. All three names print the same value because they are one object; then the pointer is moved to a different variable.

 C++
#include <iostream>

int main() {
    int target = 20;          // thermostat setting in degrees
    int* p = &target;         // p holds the address of target
    int& alias = target;      // alias is another name for target

    *p += 2;                  // write through the pointer
    std::cout << target << " " << alias << " " << *p << "\n";

    alias = 18;               // write through the reference
    std::cout << target << " " << *p << "\n";

    int other = 25;
    p = &other;               // a pointer can be moved to another object
    std::cout << *p << " " << target << "\n";
    std::cout << (p == &other) << " " << (p == &target) << "\n";
    return 0;
}

Output

22 22 22
18 18
25 18
1 0

*p += 2 changed target through its address, so target, alias and *p all print 22. alias = 18 also changed target, because a reference is not a copy. p = &other retargeted the pointer, so *p reads 25 while target stays 18, and the last line compares addresses to confirm it. There is no syntax for retargeting alias.

By value, by reference, by pointer

Three functions apply the same discount to a price; only two of them change the caller's variable. The input is three prices and a percentage.

 C++
#include <iostream>

void discountByValue(double price, double percent) {
    price -= price * percent / 100;      // changes a copy only
}

void discountByRef(double& price, double percent) {
    price -= price * percent / 100;      // changes the caller's variable
}

void discountByPtr(double* price, double percent) {
    if (price == nullptr) return;        // a pointer may be null: check it
    *price -= *price * percent / 100;
}

int main() {
    double a, b, c, percent;
    std::cin >> a >> b >> c >> percent;
    discountByValue(a, percent);
    discountByRef(b, percent);
    discountByPtr(&c, percent);
    discountByPtr(nullptr, percent);     // harmless because of the check
    std::cout << a << " " << b << " " << c << "\n";
    return 0;
}

Input given to the program: 80 80 80 25

Output

80 60 60

discountByValue discounted a copy of a and threw it away, so a is still 80. discountByRef worked on b itself. discountByPtr needed &c at the call and *price inside, and because a caller may pass nullptr, it checks first: the fourth call does nothing instead of crashing. Prefer the reference form when the argument always exists; nobody can forget an & or a *.

Returning a pointer that may be null

Six temperature readings and a limit. The search returns a pointer to the first reading above the limit, or nullptr, and pointer subtraction recovers the index.

 C++
#include <iostream>

// Returns a pointer to the first reading above limit, or nullptr if none.
const int* firstAbove(const int* readings, int count, int limit) {
    for (int i = 0; i < count; ++i) {
        if (readings[i] > limit) return &readings[i];
    }
    return nullptr;
}

int main() {
    int readings[6];
    for (int i = 0; i < 6; ++i) std::cin >> readings[i];
    int limit;
    std::cin >> limit;

    const int* hit = firstAbove(readings, 6, limit);
    if (hit != nullptr) {
        std::cout << "first reading above " << limit << " is " << *hit
                  << " at index " << (hit - readings) << "\n";
    } else {
        std::cout << "no reading above " << limit << "\n";
    }

    hit = firstAbove(readings, 6, 100);
    std::cout << (hit == nullptr ? "none above 100" : "found one") << "\n";
    return 0;
}

Input given to the program: 18 21 19 27 24 3023

Output

first reading above 23 is 27 at index 3
none above 100

One return value carries two outcomes: an address or nullptr, so the caller must test before dereferencing. hit - readings subtracts two pointers into the same array and gives the distance in elements, which is the index. The parameter type const int* lets the function read the readings but not modify them.

Pointer or reference?

QuestionPointerReference
Can it be empty?yes, nullptrno
Can it be moved to another object?yes, p = &otherno, bound once
Must it be initialised when declared?no, but an uninitialised pointer is dangerousyes
Reaching the object*p, p->memberthe name itself
Arithmetic over an array?yesno
Typical useoptional results, linked structuresparameters, aliases

Common mistakes

  • Dereferencing a pointer that is null or was never set

    Why it goes wrong: int* p; *p = 5; writes through a garbage address and *nullptr is likewise undefined behaviour: usually a crash, sometimes silent corruption.

    Fix: Initialise every pointer, to a real address or nullptr, and test before using one that might be empty.

     C++ · fix
    int* p = nullptr;
    if (p != nullptr) *p = 5;
  • Returning the address of a local variable

    Why it goes wrong: The local is destroyed when the function returns, so the caller gets a dangling pointer to memory that no longer holds the object.

    Fix: Return the value itself, or point at something that outlives the function.

     C++ · fix
    int* bad() { int x = 3; return &x; }   // dangling
    int good() { int x = 3; return x; }     // copy
  • Forgetting the & on a parameter meant to change the argument

    Why it goes wrong: void addBonus(int points, int bonus) modifies a copy; the program compiles and runs and the caller's variable simply never changes.

    Fix: Declare the parameter int& points, or pass a pointer and dereference it.

Where you use this

The everyday case is a function that must hand back more than one result or update something in place: a parser that returns the number it read and also reports how many characters it consumed takes int& consumed as a second parameter, and a swap takes both records by reference. Reading std::vector or std::string arguments by const& is so routine that it should be your default; pass by value only for small types such as int and double.

Pointers earn their place in linked structures, where each node stores the address of the next and nullptr marks the end. The recursion and DSA lesson builds a binary search tree that way, and inheritance and polymorphism uses base-class pointers so one array can hold objects of different types.

 C++ · in practice
bool parseInt(const std::string& line, int& value, int& consumed);

int value = 0, used = 0;
if (parseInt(text, value, used)) {
    // value and used were filled in by the function
}

Key points

  • &x is the address of x; a pointer stores an address and *p is the object there.
  • A reference is an alias: bound once, never null, used like the original name.
  • Arguments are copied unless the parameter is a reference or pointer; use const& to read large objects cheaply.
  • Choose a pointer when the target may be absent or must change; otherwise prefer a reference.
  • Never return the address of a local, and never delete a pointer that did not come from new.

Try it yourself

The program reads a score and a bonus, calls addBonus, and prints the score. It prints the original score because the function works on a copy. Change one line so the function updates the caller's variable and the program prints 55.

Your program
#include <iostream>

// Make this function change the caller's variable.
void addBonus(int points, int bonus) {
    points += bonus;
}

int main() {
    int points, bonus;
    std::cin >> points >> bonus;
    addBonus(points, bonus);
    std::cout << points << "\n";
    return 0;
}
Input the program receives: 40 15
Expected output: 55

Practise this

Exercises for this lesson are in the C++ practice set.

Open the C++ playground

Frequently asked questions

When should I use a pointer instead of a reference in C++?

Use a pointer when the value may be absent, when you need to change what is pointed at after initialisation, or when walking through an array or linked structure. Use a reference for parameters and local aliases where the object always exists: it cannot be null and needs no dereferencing, which removes two kinds of bugs.

Does taking a pointer with & mean I must delete it later?

No. delete is only for memory obtained with new. A pointer holding the address of a local variable, a parameter or an array element merely observes that object, which is destroyed by its own scope; deleting through such a pointer is undefined behaviour.

Progress is stored only in this browser.

How this page was checked. Every program on it was run with GCC 13 (C++17) at build time by the publishing checks, and the output shown is what it printed. Running C++ inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with GCC 13 (C++17) locally.