Question

Understanding `const int*`, `const int * const`, and `int * const` in C and C++

ccpppointersconstantsc++-faq

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 integer
  • int * const p → constant pointer to an integer
  • const int * const p → constant pointer to a constant integer

The key idea

A pointer has two separate properties:

  1. What it points to
  2. 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:

  • p is a pointer (*)
  • to const int

So: p is a pointer to a constant integer.

Another:

int * const p;

Start at p:

  • p is const
  • p is a pointer to int

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
Take Quiz

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

  • p can point to a, then later to b
  • but *p is treated as read-only through p

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

  • p must always point to a
  • but the integer stored in a can be modified through p

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;
}
  • printValue promises not to modify the integer through p
  • setToZero requires 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 * to const int * → allowed
  • const int * to int * → 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.

  • a holds 10
  • b holds 20

2. const int *p1 = &a;

p1 points to a.

  • you may later change p1 to point somewhere else
  • you may not write *p1 = ...

3. int * const p2 = &a;

p2 also points to a.

  • p2 must always point to a
  • you may change a through p2

4. const int * const p3 = &a;

p3 points to a too.

  • p3 cannot be redirected
  • *p3 cannot be modified through p3

5. p1 = &b;

This is valid because p1 is not a const pointer.

Now:

  • p1 points to b
  • p2 still points to a
  • p3 still points to a

6. *p2 = 15;

This is valid because p2 points to a non-const int.

Now:

  • a becomes 15
  • b remains 20

7. Output

std::cout << a << " " << b << '\n';

Prints:

15 20

Then:

std::cout << *p1 << " " << *p2 << " " << *p3 << '\n';
  • *p1 is 20 because p1 points to b
  • *p2 is 15 because p2 points to a
  • *p3 is 15 because p3 also points to a

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.

Take Quiz

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:

  • p itself 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:

  • p can 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

DeclarationCan change *p?Can change p?Meaning
int *pYesYespointer to mutable int
const int *pNoYespointer to const int
int * const pYesNoconst pointer to mutable int
const int * const pNoNoconst pointer to const int

const int * vs int * const

Featureconst int *int * const
Protects the pointed-to valueYesNo
Protects the pointer variableNoYes
Can be reassignedYesNo
Can modify *pNoYes

const int * vs passing by reference in C++

In modern C++, a reference is often clearer when null is not needed.

StyleExampleNotes
Pointer to constvoid f(const int *p)Can be null, must often be checked
Const referencevoid f(const int &x)Cannot be null, often simpler

If your code is C, references do not exist, so pointers are the normal tool.

Take Quiz

Cheat Sheet

Quick reading rule

Start at the variable name and read outward.

  • const int *p → p is a pointer to const int
  • int * const p → p is a const pointer to int
  • const int * const p → p is 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; ✅ allowed
  • p = 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 * const mostly 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 *.

Take Quiz

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." ]

Take Quiz

Keep learning

Related questions

View more