What Are Pointers to Const and Const Pointers?

A pointer declaration contains two objects, not one. There is the pointer itself, a variable holding an address, and there is the object sitting at that address. Each of the two can independently be made const, which is why a single keyword produces four different declarations with four different sets of rules.

A pointer to const locks the object at the far end: the pointer can be aimed somewhere else, but nothing can be written through it. A const pointer locks the near end: the address it holds is fixed for life, while the object it names can still be modified. Lock both and you get a const pointer to const.

Start from an ordinary pointer with neither lock applied, so there is something to compare against. Here a depth probe is dropped into a grain bin, moved to a second bin, and then used to correct a reading:

#include <iostream>

int main()
{
    int grainDepth{86};
    int* depthProbe{&grainDepth}; // an ordinary pointer to an ordinary int

    std::cout << "first bin: " << *depthProbe << " cm\n";

    int hopperDepth{54};
    depthProbe = &hopperDepth; // the pointer can be moved

    *depthProbe = 61; // and the object underneath can be written

    std::cout << "second bin: " << *depthProbe << " cm\n";

    return 0;
}

This prints:

first bin: 86 cm
second bin: 61 cm

Two separate capabilities were exercised there. Assigning to depthProbe changed which object the pointer refers to. Assigning to *depthProbe changed the object itself. const is how you take away either one.

Two Switches, Not One

Because the two capabilities are independent, the four combinations lay out as a grid. This table is the whole lesson; everything after it is a demonstration of one cell.

Declaration Can be aimed elsewhere Can write through it Common name
int* depthProbe Yes Yes pointer
const int* readingProbe Yes No pointer to const
int* const weldedProbe No Yes const pointer
const int* const sealedProbe No No const pointer to const

Read the second and third columns rather than memorising the names. "Pointer to const" and "const pointer" are almost the same phrase in English and nothing about them tells you which capability was removed, so the names are worth far less than the two Yes/No answers.

Reading a Pointer Declaration Right to Left

There is a mechanical rule that produces the right answer every time, without recall: a const applies to whatever sits immediately to its left, and if there is nothing to its left it applies to whatever sits immediately to its right.

Apply that to const int* readingProbe. The const has nothing on its left, so it takes the int on its right: the thing pointed at is a const int. Now apply it to int* const weldedProbe. This const has a * on its left, and * means "pointer", so the pointer is const.

Condensed to the form worth remembering: a const to the left of the * belongs to the pointed-to value, and a const to the right of the * belongs to the pointer.

That rule also explains a spelling you will meet in real code. Since a const with something on its left binds leftward, int const* and const int* are two ways of writing the identical type:

#include <iostream>

int main()
{
    int grainDepth{86};

    const int* westConst{&grainDepth}; // pointer to const int
    int const* eastConst{&grainDepth}; // the same type, written the other way round

    std::cout << *westConst << ' ' << *eastConst << '\n';

    return 0;
}

This prints:

86 86

int* const is emphatically not a third spelling of the same thing. The * is what separates the two meanings, so keep your eye on which side of it the keyword falls.

Locking the Object: Pointer to Const

A pointer to a const value, usually shortened to pointer to const, is written with const before the type. It is a normal, modifiable pointer variable, so it can be reseated as often as you like:

#include <iostream>

int main()
{
    const int siloCeiling{240};
    const int bunkerCeiling{310};

    const int* readingProbe{&siloCeiling}; // a pointer to const int

    std::cout << "ceiling now " << *readingProbe << " cm\n";

    readingProbe = &bunkerCeiling; // legal: the pointer itself is not const

    std::cout << "ceiling now " << *readingProbe << " cm\n";

    return 0;
}

This prints:

ceiling now 240 cm
ceiling now 310 cm

What it will not do is let you write through it. The following program will not compile:

int main()
{
    const int siloCeiling{240};
    const int* readingProbe{&siloCeiling};

    *readingProbe = 265; // will not compile

    return 0;
}

The compiler rejects the assignment with assignment of read-only location '* readingProbe'. Dereferencing a const int* yields a const int, and a const int is not assignable.

This is also the type a plain pointer cannot substitute for. Aiming an int* at a const object does not compile:

int main()
{
    const int siloCeiling{240};

    int* depthProbe{&siloCeiling}; // will not compile

    return 0;
}

Here the message is invalid conversion from 'const int*' to 'int*'. The rule behind it is worth stating plainly: the language will silently add const on the way in, never remove it. &siloCeiling has type const int*, and letting that become an int* would hand you a legal way to write to a const object, so the conversion is refused. Going the other direction is fine, which is the subject of the next section.

Key Concept
Read-only access is the permissive direction: `const int*` accepts targets that are writable and targets that are not. Going the other way is rejected, because a writable handle onto a locked object would defeat the lock. And neither form has anything to bind to when you point it at a literal, which has no address of its own.

The Const Belongs to the Path, Not to the Object

The name "pointer to const" suggests the object on the other end must have been declared const. It does not. What a pointer to const guarantees is that this particular pointer will not be used to modify anything. The object underneath keeps whatever mutability it was born with.

#include <iostream>

int main()
{
    int grainDepth{86}; // not const

    const int* readingProbe{&grainDepth}; // a read-only view of a writable object

    grainDepth = 91; // legal: the object is still writable through its own name

    std::cout << "probe reads " << *readingProbe << " cm\n";

    return 0;
}

This prints:

probe reads 91 cm

grainDepth changed, and readingProbe observed the change, because both names refer to the same object. The const restricted one access path, not the storage. This mirrors how a reference to const behaves, and it is what makes the type useful as a function parameter: a function that takes a const int* can be handed the address of anything at all and still be trusted not to alter it.

Locking the Pointer: Const Pointer

Move the keyword to the other side of the * and the roles swap. A const pointer holds one address for its entire lifetime. Like any other const variable it must be initialized where it is defined, since there will never be another chance to give it a value.

#include <iostream>

int main()
{
    int grainDepth{86};
    int* const weldedProbe{&grainDepth}; // a const pointer: fixed to one address for good

    *weldedProbe = 94; // legal: the object underneath is not const

    std::cout << "through the probe: " << *weldedProbe << " cm\n";
    std::cout << "through the name:  " << grainDepth << " cm\n";

    return 0;
}

This prints:

through the probe: 94 cm
through the name:  94 cm

Both lines show 94 because there is only one object; weldedProbe is simply a second way of reaching it. Nothing about int* const restricts what you write through the pointer, only where it points.

Trying to reseat one does not compile:

int main()
{
    int grainDepth{86};
    int hopperDepth{54};

    int* const weldedProbe{&grainDepth};
    weldedProbe = &hopperDepth; // will not compile

    return 0;
}

The compiler reports assignment of read-only variable 'weldedProbe'. Compare that message with the one from the earlier failure. There the read-only thing was * readingProbe, the object; here it is weldedProbe, the pointer variable. The diagnostic tells you which of the two switches you tripped.

Locking Both Ends

Writing const on both sides of the * produces a const pointer to a const value. It cannot be aimed anywhere else and cannot be written through, which leaves exactly one thing you can do with it: read.

#include <iostream>

int main()
{
    const int siloCeiling{240};
    const int* const sealedProbe{&siloCeiling}; // neither end can move

    std::cout << "sealed reading " << *sealedProbe << " cm\n";

    return 0;
}

This prints:

sealed reading 240 cm

Both const keywords are pulling their weight here, and the right-to-left rule tells you which is which without any guesswork: the first has int to its right, so the object is const; the second has * to its left, so the pointer is const.

Warning
Const on a pointer is not a promise about the object, only about that pointer. A second, non-const pointer may be aimed at the same non-const object and used to modify it while your sealed pointer watches the value change.

All Four Together

Nothing stops several pointers with different const-ness from referring to one object at the same time. This program aims all four kinds at grain bin readings and passes two of them to a function that only reads:

#include <iostream>

void reportDepth(const int* reading) // a read-only view of whatever the caller hands over
{
    std::cout << "reading " << *reading << " cm\n";
}

int main()
{
    int grainDepth{86};
    const int siloCeiling{240};

    int* plainProbe{&grainDepth};               // move it, and write through it
    const int* readingProbe{&grainDepth};       // move it, but never write through it
    int* const weldedProbe{&grainDepth};        // stuck on grainDepth, but write through it
    const int* const sealedProbe{&siloCeiling}; // stuck, and read-only

    *plainProbe = 90;
    *weldedProbe = 95;

    reportDepth(readingProbe);
    reportDepth(sealedProbe);
    reportDepth(&grainDepth); // a plain int address converts to const int* on its own

    return 0;
}

This prints:

reading 95 cm
reading 240 cm
reading 95 cm

The three calls to reportDepth are the practical payoff. Its parameter is a pointer to const, so it accepts the address of a const object, the address of a non-const object, and a pointer that is itself const, all without a cast. Declare the same parameter as a plain int* and only the last call survives: the compiler rejects the other two with invalid conversion from 'const int*' to 'int*'. Note that the first of those two is rejected even though readingProbe happens to be aimed at a writable int. What the compiler weighs is the type of the argument, not the constness of the object at the far end.

Best Practice
When a function takes a pointer to an object it only reads, declare the parameter as a pointer to const. It widens the set of arguments the function accepts and documents in the signature that nothing will be modified.

Looking Forward

Pointer parameters like reportDepth's are the subject of the next lesson, which covers passing by address in full, including what happens when the argument is null and how the choice compares with passing by reference. Later in the chapter, type deduction with pointers, references, and const explains how auto handles all four of the declarations above, which is a common source of surprise.

Key Terminology

  • Pointer to const: a modifiable pointer through which the pointed-to object cannot be written, spelled const int* or int const*
  • Const pointer: spelled int* const, aimed once at initialization and never re-aimed
  • Const pointer to const: both locks applied, spelled const int* const
  • Reseating: assigning a new address to a pointer so that it refers to a different object

Summary

  • A pointer declaration involves two objects, the pointer and the pointee, and each can be made const independently
  • const to the left of the * makes the pointed-to value const; const to the right of the * makes the pointer const
  • const int* and int const* are the same type; int* const is a different one
  • A pointer to const can be reseated but cannot be written through
  • A const pointer must be initialized where it is defined, can never be reseated, and can be written through if the object is non-const
  • A const pointer to const can only be dereferenced to read
  • A pointer to const may point at a const or a non-const object; the const constrains that access path, not the object, which can still be modified by its own name
  • A plain int* cannot be aimed at a const object, because the compiler will add const on a conversion but never remove it
  • Declare read-only pointer parameters as pointers to const so they accept const and non-const arguments alike