What Is a Pointer?

Every object a program creates occupies a numbered place in memory, and every name you write is the compiler's shorthand for one of those places. A pointer is an object whose value is such a place. Because that place is stored as a value rather than baked into an identifier, the object a pointer leads to can be chosen, and changed, while the program is running.

Compare that with an ordinary name. A variable called windSpeed refers to the same object from its declaration until it dies, and no statement can aim the identifier somewhere else halfway through. A pointer holding windSpeed's location has no such loyalty: two lines later it can hold a different object's location instead. Anything that has to decide at run time which object to read or write is eventually built on that flexibility.

Pointers have a reputation for being difficult, and most of that reputation belongs to the punctuation rather than the idea. There are two operators to learn, & and *, and a short list of rules about what a pointer may legally hold. Taken in that order, nothing here is harder than references were.

Related Content
Everything below is measured against lvalue references, so if binding, reseating, and const aliases feel hazy, revisit those earlier lessons before continuing here.

Turning a Name Into a Location, and Back Again

Two unary operators move between an object and the place it lives:

  • The address-of operator (&), written in front of an object, hands back a pointer holding that object's location. It does not produce a bare number, and C++ has no such thing as an address literal.
  • The dereference operator (*), sometimes called the indirection operator, written in front of a location, hands back the object stored there as an lvalue.

Each undoes the other. Take an object's address and immediately dereference it, and you arrive back where you began:

#include <iostream>

int main()
{
    int windSpeed{34};

    std::cout << windSpeed << '\n';     // reach the object by its name
    std::cout << *(&windSpeed) << '\n'; // reach the object by its location

    return 0;
}
34
34

*(&windSpeed) is a roundabout way to write windSpeed, and no real program contains it. It earns its place here only as proof that the two operators are inverses. The parentheses are optional; they simply separate the two steps for the reader.

An address can be streamed to std::cout, where it prints as a hexadecimal number. Which number you get is decided by the operating system as the program starts, so it differs between runs and between machines, and nothing in this lesson depends on a specific one. When an object spans several bytes, the address you obtain is the first of them.

Both symbols are overloaded, and what they mean is settled by where they appear and how many operands they take:

Written as Context Meaning
int& gauge in a declaration declares an lvalue reference
&windSpeed one operand, in an expression address-of, producing a pointer
mask & bits two operands bitwise AND
int* tracker in a declaration declares a pointer
*tracker one operand, in an expression dereference, producing the object
hours * rate two operands multiplication

The two declaration rows are the ones that catch people out. In int* tracker nothing is being dereferenced, and in int& gauge nothing's address is being taken; in both, the symbol is part of the type being written.

Declaring a Pointer

A pointer type is spelled as the type of the pointed-to object followed by an asterisk. int* is the type "pointer to int" and double* is the type "pointer to double"; they are separate, unrelated types:

int* tracker{&windSpeed};    // tracker is a pointer to int
double* moisture{&humidity}; // moisture is a pointer to double

Spoken shorthand runs the other way round, and it is worth getting used to now because const will make the wording matter later.

Nomenclature
"An int pointer" is everyday shorthand for "a pointer to an int". The type name comes first when spoken, even though it is the pointed-to type that is being named.
Best Practice
int* tracker and int *tracker declare exactly the same variable, so this is a readability decision rather than a correctness one. Settle on the first spelling and stay with it: it keeps the asterisk with the rest of the type, which is where the type information belongs, and it lines up with how int& is written for references.
Warning
An asterisk attaches to a single declarator, not to the whole statement. int* first, second; produces a pointer named first and a plain int named second, which is rarely what the author had in mind. Repeating the asterisk as int* first, * second; fixes it, and giving each variable its own statement means the question never comes up.

Give a Pointer a Value Before You Read One

Declaring a pointer sets aside room for an address; it does not put one there. A pointer left uninitialised is a wild pointer, and its value is whatever bit pattern that memory happened to be holding already. Dereferencing a wild pointer is undefined behaviour, and it is the flavour of undefined behaviour that quietly appears to work on the machine you wrote it on.

There are three ways a pointer declaration can turn out:

Declaration What the pointer holds Result of dereferencing it
int* tracker; leftover bits, meaning nothing (a wild pointer) undefined behaviour
int* tracker{}; no address at all (a null pointer, covered next lesson) undefined behaviour
int* tracker{&windSpeed}; the location of windSpeed the object, for as long as windSpeed lives

Only the third row is usable immediately, but the second row is safe to hold on to, because a pointer that holds no address can be tested for that condition before anything dereferences it. The first row cannot be tested for anything.

Best Practice
Never leave a pointer declaration without an initialiser. When you already know which object it should lead to, put that address in the declaring statement; when you do not know yet, write {} so the pointer starts out empty and can be checked before use.

The Declared Type Must Match the Object at That Address

A pointer's type is a promise about what lives at the address it holds, and the compiler enforces it. Feeding the address of a double to an int* is rejected, and so is the reverse. This program is broken on purpose:

#include <iostream>

int main()
{
    int windSpeed{34};
    double humidity{61.5};

    int* tracker{&humidity};      // will not compile
    double* moisture{&windSpeed}; // will not compile

    std::cout << *tracker << ' ' << *moisture << '\n';

    return 0;
}
s.cpp: In function 'int main()':
s.cpp:8:18: error: cannot convert 'double*' to 'int*' in initialization
    8 |     int* tracker{&humidity};      // will not compile
      |                  ^~~~~~~~~
      |                  |
      |                  double*
s.cpp:9:22: error: cannot convert 'int*' to 'double*' in initialization
    9 |     double* moisture{&windSpeed}; // will not compile
      |                      ^~~~~~~~~~
      |                      |
      |                      int*

Read the diagnostic closely, because it settles a question this lesson raised earlier. GCC labels &humidity as being of type double* and &windSpeed as being of type int*. The address-of operator really does produce a pointer, complete with a pointer type, rather than a numeric value that happens to be an address.

The same type rule blocks the obvious shortcut of writing an address by hand:

int* tracker{7};        // rejected: 7 is an int, not an int*
int* guess{0x5A3E10};   // rejected: hexadecimal notation does not make an int into an address

GCC reports invalid conversion from 'int' to 'int*' for both lines. There is exactly one literal a pointer will accept, and the next lesson introduces it.

Two Different Things an Assignment Can Mean

Because a pointer is an object holding a location, there are two objects in play whenever you assign: the pointer and the thing it leads to. Which one you change is decided entirely by whether the asterisk is present:

Statement Which object is written Effect
tracker = &gustSpeed; the pointer tracker now leads to gustSpeed; nothing else changes
*tracker = 61; the object at that location gustSpeed becomes 61; tracker still leads to it

Swapping the two by mistake does not silently misbehave, because the types disagree: *tracker = &gustSpeed; asks to store an address inside an int, and tracker = 61; asks to store an int inside a pointer. Both are rejected at compile time. One program exercises the correct pair:

#include <iostream>

int main()
{
    int windSpeed{34};
    int gustSpeed{58};

    int* tracker{&windSpeed};
    std::cout << *tracker << '\n';

    tracker = &gustSpeed;
    std::cout << *tracker << '\n';

    *tracker = 61;
    std::cout << gustSpeed << '\n';
    std::cout << windSpeed << '\n';

    return 0;
}
34
58
61
34

The last line is the one to dwell on. windSpeed still reads 34 because by the time the write happened, tracker was aimed elsewhere. A pointer gives you access to exactly one object at a time: whichever one its current value names.

Key Concept
Read a bare tracker as "which object", and *tracker as "the object". Assigning to the first changes the pointer's aim; assigning to the second changes the value stored at the far end of it, and leaves the aim alone.

Every Pointer Is the Same Size

Since dereferencing an int* yields a four-byte int and dereferencing a long double* yields something much larger, it is tempting to assume the pointers differ in size too. They do not. A pointer's value is an address, and the number of bits it takes to name a location is a property of the machine rather than of whatever is stored there:

#include <iostream>

int main()
{
    char* codeSlot{};
    int* speedSlot{};
    long double* driftSlot{};

    std::cout << sizeof(char) << ' ' << sizeof(int) << ' ' << sizeof(long double) << '\n';
    std::cout << sizeof(codeSlot) << ' ' << sizeof(speedSlot) << ' ' << sizeof(driftSlot) << '\n';

    return 0;
}
1 4 16
8 8 8

On the 64-bit Linux target used here the pointed-to types range from one byte to sixteen, and all three pointers occupy eight. Eight is what a 64-bit executable uses, because a 64-bit address is eight bytes wide. Compile the same file as a 32-bit executable and every line of the second row becomes 4, still regardless of what each pointer refers to.

Pointer or Reference?

An lvalue reference and a pointer both give you a second route to an object you do not own. Below, three names lead to a single int, and a write through any of them is visible through all of them:

#include <iostream>

int main()
{
    int windSpeed{34};
    int& gauge{windSpeed};
    int* tracker{&windSpeed};

    std::cout << windSpeed << ' ' << gauge << ' ' << *tracker << '\n';

    gauge = 47;
    std::cout << windSpeed << ' ' << gauge << ' ' << *tracker << '\n';

    *tracker = 52;
    std::cout << windSpeed << ' ' << gauge << ' ' << *tracker << '\n';

    return 0;
}
34 34 34
47 47 47
52 52 52

Three columns, one object. The indirection is identical; what differs is how much of it you have to write down. A reference performs the address-taking and the dereferencing for you, silently, at every use. A pointer makes you write & once and * every time, and hands you capabilities in exchange:

Question Lvalue reference Pointer
Is it an object? no, it is an alias yes, with its own storage and address
Must it be initialised? yes, the language requires it no, though you always should
Can it be re-aimed later? no, the binding is permanent yes, by assigning a new address
Can it refer to nothing? no, it is always bound yes, that is what a null pointer is
Syntax at the point of use none, it reads like the object * to reach the object
Failure modes dangling only dangling, wild, and null

The last row is why references remain the default. A pointer can go wrong in ways a reference cannot, so the extra capability has to be worth something before you reach for it.

When the Object Dies First

A pointer stores a location, and it has no idea whether the object that used to live there still does. A dangling pointer is one holding the address of an object that has been destroyed. Dereferencing it is undefined behaviour, exactly as with a wild pointer, because there is nothing valid at the far end to read.

The program below is wrong on purpose, and GCC is sharp enough to say so:

#include <iostream>

int main()
{
    int windSpeed{34};
    int* tracker{&windSpeed};

    {
        int squallSpeed{72};
        tracker = &squallSpeed;

        std::cout << *tracker << '\n';
    }

    std::cout << *tracker << '\n';

    return 0;
}
s.cpp: In function 'int main()':
s.cpp:15:30: warning: using dangling pointer 'tracker' to 'squallSpeed' [-Wdangling-pointer=]
   15 |     std::cout << *tracker << '\n';
      |                              ^~~~
s.cpp:9:13: note: 'squallSpeed' declared here
    9 |         int squallSpeed{72};
      |             ^~~~~~~~~~~

squallSpeed is destroyed at the closing brace of the inner block, and tracker outlives it while still holding its address. The first dereference is fine and prints 72. The second is undefined behaviour, so what it prints is not documented here; the standard makes no promise about it, and neither should a lesson.

Notice that the diagnostic is a warning rather than an error. The compiler spotted this case because both the pointer and the dead object are visible in one function, which will not be true once addresses start travelling across function boundaries. Treat the warning as a lucky catch rather than a safety net.

Two Grades of Trouble
Reading through a pointer that leads nowhere valid is undefined behaviour, and all three broken states qualify: wild, dangling, and null. The standard grades the remaining operations more gently. Copy such a pointer into another variable, or compare it against something, and the result is implementation-defined instead: your compiler will do something consistent, but the language declines to say what, so portable code cannot rely on it. Overwriting a broken pointer with a fresh address is always fine, because storing into it never inspects what was already there.

Looking Forward

The next lesson covers null pointers, the one value a pointer can hold that reliably means "nothing here", along with the checks that make that state usable rather than dangerous. After that come pointers and const, and then pass by address, which is where pointers start replacing references in function parameter lists for arguments that may legitimately be absent.

The pointers in this chapter are sometimes called raw pointers, or occasionally dumb pointers, to distinguish them from the smart pointer types in a later chapter. Smart pointers wrap a raw pointer in a class that manages the lifetime of what it refers to, which removes most of the failure modes listed above.

Key Terminology

  • Address-of operator (&): applied to an object, produces a pointer holding that object's location
  • Dereference operator (*): applied to a pointer, produces the object at the location it holds, as an lvalue
  • Pointer: an object whose value is the location of another object
  • Pointer type: a type such as int*, naming what may be found at the address a pointer of that type holds
  • Wild pointer: an uninitialised pointer, holding whatever bits were already in its storage
  • Dangling pointer: a pointer holding the address of an object that has since been destroyed
  • Raw pointer: a plain language-level pointer, as opposed to a smart pointer class

Summary

  • A pointer is an object whose value is a memory location, so which object it leads to is decided at run time rather than at compile time
  • &object produces a pointer to that object; *pointer produces the object at the address held, as an lvalue; the two operators are inverses
  • A pointer's declared type fixes what may live at the address it holds, and the compiler rejects a mismatch such as int* tracker{&humidity}
  • Assigning to tracker re-aims the pointer, while assigning to *tracker writes through it to the object; the asterisk decides which
  • An uninitialised pointer is a wild pointer, so give every pointer either an address or {} in its declaring statement
  • All pointers on a given target are the same width, eight bytes for a 64-bit build and four for a 32-bit one, no matter how large the pointed-to type is
  • References and pointers both provide indirect access to an object, but only a pointer can be re-aimed, refer to nothing, or be left uninitialised
  • A pointer whose object has been destroyed is dangling; dereferencing it is undefined behaviour, while merely copying or comparing a broken pointer is implementation-defined rather than undefined