Const Correctness with Pointers
Distinguish between pointers to const data and const pointer variables.
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.
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.
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.
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*orint 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
constindependently constto the left of the*makes the pointed-to value const;constto the right of the*makes the pointer constconst int*andint const*are the same type;int* constis 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
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Const Correctness with Pointers - Quiz
Test your understanding of the lesson.
Practice Exercises
Pointers and Const
Master the four combinations of pointers and const: non-const pointer to non-const, pointer to const, const pointer, and const pointer to const. Learn when to use each type.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!