Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 2 of 6

A pointer is a variable whose value is an address.

#include <iostream>

int main() {
    int x = 42;
    int* p = &x;      // p holds the address of x

    std::cout << x << "\n";      // the value
    std::cout << *p << "\n";     // the value at the address p holds
    return 0;
}
42
42

Two operators, and they are opposites:

  • &x — "the address of x". Takes you out of a variable to where it lives.
  • *p — "the object at address p". Takes you in, to the value. This is dereferencing.

int* p reads as "p is a pointer to int." The type matters: a pointer knows the type of what it points at, which is how *p knows to read 4 bytes as an int rather than 8 bytes as a double.

Writing through a pointer

Dereferencing is not read-only. *p names the object, so it can go on the left of =:

#include <iostream>

int main() {
    int x = 42;
    int* p = &x;

    *p = 99;                      // writes to x, through p
    std::cout << x << "\n";

    x = 7;                        // and the other direction
    std::cout << *p << "\n";
    return 0;
}
99
7

There is only one int here. x and *p are two names for it.

Where to put the asterisk

These three declare the same thing:

int* p;
int *p;
int * p;

The compiler ignores the spacing. This codebase uses int* p, because the * is part of the type: p is a pointer-to-int.

But that style has one trap, and it is worth knowing before it bites you:

#include <iostream>

int main() {
    int a = 1, b = 2;

    int* p, q;        // p is int*, q is a plain int — NOT two pointers
    p = &a;
    q = b;

    std::cout << *p << " " << q << "\n";
    return 0;
}
1 2

The * binds to the declarator, not the type, so it applies only to p. Declare one pointer per line and the question never arises.

Pointers are variables too

A pointer can be reassigned to point somewhere else:

#include <iostream>

int main() {
    int a = 1;
    int b = 2;
    int* p = &a;

    std::cout << *p << "\n";
    p = &b;               // p now points at b; a is untouched
    std::cout << *p << "\n";
    std::cout << a << "\n";
    return 0;
}
1
2
1

Note the difference between p = &b and *p = b. The first re-points the pointer. The second writes b's value into whatever p currently points at. Mixing these up is one of the most common beginner bugs in the language, and the compiler will not catch it — both are legal.

nullptr: pointing at nothing

A pointer has to hold something. When there is no object to point at, point at nothing deliberately:

#include <iostream>

int main() {
    int* p = nullptr;

    if (p == nullptr) {
        std::cout << "p points at nothing\n";
    }

    // A pointer converts to false when null, true otherwise — the idiomatic check.
    if (!p) {
        std::cout << "still nothing\n";
    }

    int x = 5;
    p = &x;
    if (p) {
        std::cout << "now it points somewhere: " << *p << "\n";
    }
    return 0;
}
p points at nothing
still nothing
now it points somewhere: 5

Dereferencing a null pointer is undefined behaviour (lesson 8.7). In practice it usually crashes immediately — which is the good case, because the crash is at the mistake. Check before you dereference anything that might be null.

Use nullptr, not NULL and not 0. nullptr has a distinct type that cannot be mistaken for an integer, which matters for overload resolution (lesson 5.6). NULL is a leftover from C.

Uninitialised is worse than null

int* p;           // holds whatever bytes were already there
*p = 42;          // writes to an arbitrary address

An uninitialised pointer holds garbage. Dereferencing it writes to some arbitrary address: it might crash, or it might silently corrupt something you only find out about an hour later.

This is undefined behaviour, and note what the compiler does about it — a warning, not an error:

warning: 'p' is used uninitialized [-Wuninitialized]

It still produces a runnable program. And -Wuninitialized only catches the cases the compiler can see through; hand the pointer to another function first and the warning disappears while the bug does not. Do not rely on it. Make it a habit instead: every pointer gets a value at declaration, even if that value is nullptr.

Compiling with -Werror (lesson 11.1) turns warnings like this into hard errors, which is the right setting for real work.

What a pointer costs

#include <iostream>

int main() {
    char c = 'x';
    double d = 3.5;
    int big[1000] = {};

    std::cout << sizeof(&c) << " " << sizeof(&d) << " " << sizeof(&big) << "\n";
    return 0;
}
8 8 8

Eight bytes each. Passing a pointer to a 4000-byte array copies 8 bytes, which is the point of lesson 5.3 and why const T& is the default for large parameters.

Try it

Write void swap_ints(int* a, int* b) that exchanges the two values its arguments point at. Call it and show that the caller's variables really changed. Then make it safe against a null argument.

Checkpoint

  • A pointer holds an address; int* p = &x; makes p point at x.
  • & gets an address, * gets the object at one. They are inverses.
  • *p = v writes through the pointer; p = &v re-points it. Very different.
  • Declare one pointer per line: in int* p, q; only p is a pointer.
  • Use nullptr for "points at nothing", and check before dereferencing.
  • Dereferencing a null or uninitialised pointer is undefined behaviour.
  • A pointer is 8 bytes whatever it points at.

Questions about this lesson

Log in to ask a question.