C++ / REFERENCES, POINTERS, AND NULL
const pointers versus pointers to const
Decide whether const should freeze a pointer's address, the object it points at, or both, and read any declaration to tell which it does.
What you will learn
- Read pointer declarations right to left to see what const binds to
- Put const left of * to freeze the pointee, right of * to freeze the pointer
- Pass read-only data as const T* and rely on the safe T* to const T* conversion
- Recognize the alias trap: const IntPtr means int* const, not const int*
Understanding const pointers versus pointers to const
A pointer variable owns two separate things: the address stored inside it, and write access to the object living at that address. const can be applied to either one, which is why C++ has two different declarations that beginners collapse into one. const int* pc is a pointer to const: you may re-aim pc or increment it, but *pc = 5 is rejected. int* const cp is a const pointer: cp is fixed at initialization and can never point elsewhere, yet *cp = 5 is perfectly legal.
The mental model is that the * is a wall. A const to the left of the star belongs to the pointee type; a const to the right of the star belongs to the pointer variable itself, which is called top-level const. Read outward from the identifier, right to left: in int* const cp, the token next to cp is const, so cp is const, and it is a pointer to int. Since const int and int const name the same type, const int* and int const* are identical types, and writing the const on the right consistently makes the right-to-left rule purely mechanical.
The two kinds of const behave differently because they mean different things to the type system. Top-level const is a property of one variable, so it vanishes when the pointer's value is copied; that is why the compiler ignores it in a function signature and why void f(int*) and void f(int* const) declare the same function. Low-level const is part of the type and travels with the value, so int* converts implicitly to const int* — adding a restriction is always safe — while const int* to int* is refused, because that conversion could hand out a writable path to a genuinely const object, and writing through it would be undefined behavior.
<iostream>
int main() {
int a = 10;
int b = 20;
const int* pc = &a; // pointer to const: read-only through pc
int* const cp = &a; // const pointer: cp can never be re-aimed
pc = &b; // ok: the pointer itself is not const
// *pc = 99; // error: assignment of read-only location
*cp = 99; // ok: the pointee is a plain int
// cp = &b; // error: assignment of read-only variable 'cp'
const int* const both = &a; // neither half can change
std::cout << "a = " << a << '\n';
std::cout << "*pc = " << *pc << " (pc now aims at b)\n";
std::cout << "*cp = " << *cp << '\n';
std::cout << "*both = " << *both << '\n';
}
In a pointer declaration the * is the divider: const to its left constrains the object pointed at, const to its right constrains the pointer variable, and the two choices are independent.
Worked examples
const T* in a function parameter
Shows that a pointer to const can still be moved, and that the implicit conversion only runs in the safe direction.
<iostream>
int sum(const int* data, int n) { // promises no writes through data
int total = 0;
for (const int* p = data; p != data + n; ++p) total += *p;
return total;
}
void zeroFirst(int* data) { *data = 0; }
int main() {
int nums[] = {1, 2, 3, 4};
const int frozen[] = {5, 6, 7};
std::cout << sum(nums, 4) << '\n'; // int* converts to const int*
std::cout << sum(frozen, 3) << '\n'; // already a const int*
zeroFirst(nums);
// zeroFirst(frozen); // error: const int* to int* refused
std::cout << nums[0] << '\n';
}
Example explained
Line 1++p on a const int* compiles because the restriction sits on *p, not on p.
Line 2sum(nums, 4) works through the implicit int* to const int* conversion, which only adds a restriction.
Line 3sum(frozen, 3) is possible only because the parameter is const; a plain int* parameter would reject a const array.
Line 4zeroFirst(frozen) is refused because the reverse conversion would give the function a writable path to const data.
The type-alias trap
Demonstrates that const applied to a pointer alias lands on the pointer, not on the pointee.
<iostream>
using IntPtr = int*;
int main() {
int x = 1;
int y = 2;
const IntPtr p = &x; // this is int* const, not const int*
*p = 42; // allowed: the pointee is a plain int
// p = &y; // error: p is const
const int* q = &x;
q = &y; // allowed: q itself is not const
// *q = 42; // error: read-only location
std::cout << "x = " << x << '\n';
std::cout << "*p = " << *p << '\n';
std::cout << "*q = " << *q << '\n';
}
Example explained
Line 1const IntPtr applies const to the whole alias, so it becomes top-level const on the pointer; const cannot reach inside the alias to the int.
Line 2*p = 42 compiles and changes x, proving a const pointer says nothing about its pointee.
Line 3q = &y compiles because pointer-to-const restricts only *q, which is why *q prints 2 rather than 42.
Line 4Spelling it int* const p, or not aliasing pointer types at all, removes the ambiguity.
Important notes
const int* and int const* are the same type; what matters is the const's position relative to the *, not relative to int.
A pointer to const restricts what you may do through that pointer; it does not make the object const. Another non-const path to the same object may legally change it.
Common mistakes
Reading const int* p as "p is constant" and then trying *p = 0, which fails with an assignment-to-read-only error while p = &b and ++p compile without complaint.
Using const_cast<int*> or a C-style cast to squeeze a const int* into an int* parameter: if the object really was declared const, the write is undefined behavior and often crashes because the object sits in read-only memory.
Writing typedef char* CharPtr; const CharPtr s = buf; and expecting immutable text, when the type is char* const and s[0] = 'X' still modifies the buffer.
Try it yourself
Change, predict, then run
Declare int x = 5; int y = 9; and three pointers aimed at x: a const int*, an int* const, and a const int* const. Then add the assignments p = &y; and *p = 7; for each one, comment them back out until you can predict which four of the six lines the compiler rejects.
Open the C++ workspaceCheck your understanding
A function is declared void f(const int* p). What does that const actually guarantee?
- The object p points at cannot change for as long as f is running.
- f will not write to the object through p, but the object itself may be non-const and change by other means.
- p cannot be made to point at a different object inside f.
- f can only be called with the address of an object that was declared const.
Show answer
The const applies to the pointee type, so *p = ... will not compile inside f; that is a promise about one access path. Option 0 is the tempting one but wrong: a non-const reference elsewhere, or code that f itself calls, can still modify the same object, so const is not a stability guarantee. Option 2 describes int* const p, and option 3 fails because int* converts implicitly to const int*.