Question
Understanding `const int*`, `const int * const`, and `int * const` in C and C++
Question
I often get confused about how to use const int *, const int * const, and int * const correctly in C or C++. What are the rules that define what is allowed and what is not?
I want to understand the practical do's and don'ts for:
- assigning these pointer types
- modifying the pointed-to value
- changing the pointer itself
- passing them to functions
- returning them from functions
For example, what operations are valid or invalid for these forms?
const int *p1;
const int * const p2 = nullptr;
int * const p3 = nullptr; // or another valid initialization
Short Answer
By the end of this page, you will understand how const applies to pointers in C and C++, how to read declarations from right to left, and what each form allows you to change: the value, the pointer, both, or neither. You will also see how these types behave in assignments and function parameters.
Concept
const with pointers can apply to two different things:
- the value being pointed to
- the pointer variable itself
That is why pointer declarations with const can look similar but behave differently.
Consider these three common forms:
const int *p;
int * const p;
const int * const p;
They mean:
const int *p→ pointer to a constant integerint * const p→ constant pointer to an integerconst int * const p→ constant pointer to a constant integer
The key idea
A pointer has two separate properties:
- What it points to
- Whether the pointer itself can be changed
const can lock either one.
Why this matters
This is important in real programs because developers use const to:
- prevent accidental modification of data
- express intent clearly in function APIs
- allow safe access to shared data
- make code easier to reason about
- catch mistakes at compile time
For example, if a function should read a number but not change it, const int * is a good parameter type. If a pointer must always point to the same object after initialization, int * const can enforce that.
Reading rule
A helpful rule is: read the declaration starting from the variable name, then move outward.
Example:
const int *p;
Start at p:
pis a pointer (*)- to
const int
So: p is a pointer to a constant integer.
Another:
int * const p;
Start at p:
pis constpis a pointer toint
So: p is a constant pointer to an integer.
Mental Model
Think of a pointer as a sticky note with an address written on it.
There are two things you might want to protect:
- the paper the sticky note points to
- the address written on the sticky note
const int *p
The address on the sticky note can change, but the paper is read-only through this pointer.
- you can move the sticky note to another address
- you cannot edit the value through this pointer
int * const p
The sticky note's address is permanent, but the paper is editable.
- you cannot move the sticky note to a new address
- you can edit the value at that address
const int * const p
Both are locked.
- you cannot move the sticky note
- you cannot edit the value through this pointer
This mental model helps because most confusion comes from mixing up:
- changing where the pointer points
- changing the data at that location
Syntax and Examples
Core syntax
const int *p; // pointer to const int
int const *p; // same meaning as above
int * const p = &x; // const pointer to int
const int * const p = &x; // const pointer to const int
const int *p and int const *p mean the same thing. The position of const around the base type does not change the meaning here.
Example 1: pointer to const int
int a = 10;
int b = 20;
const int *p = &a;
p = &b; // valid: pointer can change
//*p = 30; // invalid: cannot modify value through p
Explanation
pcan point toa, then later tob- but
*pis treated as read-only throughp
Example 2: const pointer to int
int a = 10;
int b = 20;
int * const p = &a;
*p = 30; // valid: pointed-to value can change
//p = &b; // invalid: pointer itself cannot change
Explanation
pmust always point toa- but the integer stored in
acan be modified throughp
Example 3: const pointer to const int
int a = 10;
int b = 20;
const int * const p = &a;
//*p = 30; // invalid: cannot modify value
//p = &b; // invalid: cannot change pointer
Explanation
- neither the target nor the pointer can be changed through
p
Function parameter examples
void printValue(const int *p) {
std::cout << *p << '\n';
}
void setToZero(int *p) {
*p = 0;
}
printValuepromises not to modify the integer throughpsetToZerorequires a modifiable integer
Valid and invalid assignments
int x = 5;
const int cx = 10;
int *p1 = &x; // valid
const int *p2 = &x; // valid
const int *p3 = &cx; // valid
// int *p4 = &cx; // invalid: drops const protection
You can usually convert:
int *toconst int *→ allowedconst int *toint *→ not allowed
That rule prevents modifying a truly constant object through a non-const pointer.
Step by Step Execution
Consider this example:
#include <iostream>
int main() {
int a = 10;
int b = 20;
const int *p1 = &a;
int * const p2 = &a;
const int * const p3 = &a;
p1 = &b;
*p2 = 15;
std::cout << a << " " << b << '\n';
std::cout << *p1 << " " << *p2 << " " << *p3 << '\n';
}
Step by step
1. int a = 10; int b = 20;
Two integers are created.
aholds10bholds20
2. const int *p1 = &a;
p1 points to a.
- you may later change
p1to point somewhere else - you may not write
*p1 = ...
3. int * const p2 = &a;
p2 also points to a.
p2must always point toa- you may change
athroughp2
4. const int * const p3 = &a;
p3 points to a too.
p3cannot be redirected*p3cannot be modified throughp3
5. p1 = &b;
This is valid because p1 is not a const pointer.
Now:
p1points tobp2still points toap3still points toa
6. *p2 = 15;
This is valid because p2 points to a non-const int.
Now:
abecomes15bremains20
7. Output
std::cout << a << " " << b << '\n';
Prints:
15 20
Then:
std::cout << *p1 << " " << *p2 << " " << *p3 << '\n';
*p1is20becausep1points tob*p2is15becausep2points toa*p3is15becausep3also points toa
So it prints:
20 15 15
Real World Use Cases
1. Read-only function parameters
void displayScore(const int *score) {
std::cout << *score << '\n';
}
Use const int * when a function should inspect data without modifying it.
2. Protecting API inputs
In larger systems, functions often receive pointers to shared data. Marking data as const prevents accidental writes.
bool isPositive(const int *value) {
return *value > 0;
}
3. Fixed internal references
A const pointer can be useful when a pointer member or local alias should never be redirected after setup.
int value = 42;
int * const fixedPtr = &value;
This is less common in everyday application code than const int *, but it can still be useful when pointer reassignment would be a bug.
4. Read-only handles to configuration data
void printConfigValue(const int *configValue) {
std::cout << *configValue << '\n';
}
This expresses: “I need access to the value, but I will not change it.”
5. Working with arrays
Pointers are often used to traverse arrays.
void printArray(const int *arr, int size) {
for (int i = 0; i < size; ++i) {
std::cout << arr[i] << ' ';
}
}
Here, arr can move internally as a pointer value if needed, but the data should not be modified.
Real Codebase Usage
In real projects, const with pointers is mostly about communicating intent and preventing bugs.
Common patterns
Guarding inputs
void logUserId(const int *userId) {
if (userId == nullptr) return;
std::cout << *userId << '\n';
}
This combines:
- a pointer validity check
- read-only access
Validation before mutation
bool setIfPositive(int *value, int newValue) {
if (value == nullptr) return false;
if (newValue < 0) return false;
*value = newValue;
return true;
}
When mutation is intended, use int *, not const int *.
Separating readers from writers
Many codebases use one function for reading and another for writing.
int getValue(const int *p) {
return *p;
}
void updateValue(int *p, int value) {
*p = value;
}
This makes APIs clearer.
Const correctness across layers
If one function receives const int *, helper functions it calls often also need to accept const int *.
void printTwice(const int *p) {
std::cout << *p << '\n';
std::cout << *p << '\n';
}
This is called const correctness: once data is read-only in one place, that property should be preserved where possible.
Early returns with pointer parameters
void show(const int *p) {
if (!p) return;
std::cout << *p << '\n';
}
This is a very common defensive style in C and C++ code.
Common Mistakes
1. Confusing “const data” with “const pointer”
Broken code:
int a = 10;
int b = 20;
int * const p = &a;
p = &b; // error
Why it fails:
pitself is const- the pointer cannot be reassigned
2. Trying to modify data through const int *
Broken code:
int a = 10;
const int *p = &a;
*p = 99; // error
Why it fails:
- the pointed-to value is treated as read-only through
p
3. Removing const protection in assignment
Broken code:
const int x = 5;
int *p = &x; // error
Why it fails:
- this would allow modification of a constant object
Correct version:
const int *p = &x;
4. Forgetting that const pointer variables must be initialized
Broken code:
int * const p; // error
Why it fails:
pcan never be changed later- so it must be given an address immediately
Correct version:
int value = 10;
int * const p = &value;
5. Assuming const int * means the original object can never change
int a = 10;
const int *p = &a;
a = 50; // valid
Important detail:
const int *only prevents modification through that pointer- it does not make the original object globally immutable
6. Overusing casts to silence errors
Broken idea:
const int x = 5;
int *p = (int *)&x; // dangerous
Why to avoid it:
- casting away const can lead to undefined behavior if the object is truly const
- if the compiler rejects an assignment, the type design is probably wrong
Comparisons
Pointer constness comparison
| Declaration | Can change *p? | Can change p? | Meaning |
|---|---|---|---|
int *p | Yes | Yes | pointer to mutable int |
const int *p | No | Yes | pointer to const int |
int * const p | Yes | No | const pointer to mutable int |
const int * const p | No | No | const pointer to const int |
const int * vs int * const
| Feature | const int * | int * const |
|---|---|---|
| Protects the pointed-to value | Yes | No |
| Protects the pointer variable | No | Yes |
| Can be reassigned | Yes | No |
Can modify *p | No | Yes |
const int * vs passing by reference in C++
In modern C++, a reference is often clearer when null is not needed.
| Style | Example | Notes |
|---|---|---|
| Pointer to const | void f(const int *p) | Can be null, must often be checked |
| Const reference | void f(const int &x) | Cannot be null, often simpler |
If your code is C, references do not exist, so pointers are the normal tool.
Cheat Sheet
Quick reading rule
Start at the variable name and read outward.
const int *p→pis a pointer to const intint * const p→pis a const pointer to intconst int * const p→pis a const pointer to const int
What each form allows
int *p;
- change
p✅ - change
*p✅
const int *p;
- change
p✅ - change
*p❌
int * const p = &x;
- change
p❌ - change
*p✅
const int * const p = &x;
- change
p❌ - change
*p❌
Assignment rules
int *p;
const int *cp;
cp = p;✅ allowedp = cp;❌ not allowed
Initialization rule
Const pointers must be initialized:
int x = 1;
int * const p = &x;
Function parameter guideline
- use
const int *when the function should only read - use
int *when the function needs to modify - use
int * constmostly for local variables, less often in function parameters
Same meaning
const int *p;
int const *p;
These are equivalent.
FAQ
What is the easiest way to read pointer const declarations?
Start at the variable name and move outward. This usually makes the meaning clear.
Is const int *p the same as int const *p?
Yes. Both mean “pointer to const int.”
Can I assign an int * to a const int *?
Yes. That is safe because it adds const protection.
Can I assign a const int * to an int *?
No. That would remove const protection and could allow illegal modification.
Does const int *p mean the integer can never change?
No. It only means you cannot change it through p. The original variable may still change through another non-const name or pointer.
Why must int * const p be initialized immediately?
Because the pointer itself cannot be changed later, so it needs its final address at declaration time.
Should I use pointers or references in C++ for read-only parameters?
If null is not a valid state, const int & is often clearer in C++. If null is meaningful, use const int *.
Mini Project
Description
Build a small C++ program that demonstrates all three pointer const forms using a few integers. The project helps you practice which operations are allowed, which are rejected by the compiler, and how these types are commonly used in functions.
Goal
Create a program that reads values through const-safe pointers and modifies values only through pointers that allow mutation.
Requirements
[
"Declare one normal integer and one const integer.",
"Create examples of const int *, int * const, and const int * const.",
"Show at least one valid reassignment and one valid value modification.",
"Add two functions: one that reads an integer through a pointer and one that updates an integer through a pointer.",
"Include commented-out invalid lines to show what the compiler would reject."
]
Keep learning
Related questions
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.