C / POINTERS
const placement: pointer, pointee or both
Decide whether the pointee, the pointer, or both are read-only, and read any const pointer declaration right to left without guessing.
What you will learn
- Tell apart const int *p, int *const p and const int *const p by reading right to left
- Know that const int * blocks writes through that pointer, not changes to the object
- Write read-only pointer parameters as const T * so callers can pass T * as well
- Spot the typedef trap where const lands on the pointer instead of the pointee
Understanding const placement: pointer, pointee or both
A pointer declaration contains two things that can be qualified: the pointer variable and the object it points at. The `*` is the divider. A const to the left of the `*` qualifies the pointee; a const to the right of the `*` qualifies the pointer itself. `const int *p` and `int const *p` are the same type, because const attaches to whatever sits immediately to its left and only falls back to the type on its right when it is the leftmost token.
Read the declaration right to left starting at the identifier and the ambiguity disappears: `int *const p` is "p is a const pointer to int", while `const int *p` is "p is a pointer to const int". The second form is a claim about an access path, not about storage. `const int *p = &x;` does not freeze x; it only means the compiler rejects writes that travel through p, while `x = 9;` or any plain `int *` aimed at x still works.
That asymmetry explains the conversion rules: `int *` converts to `const int *` implicitly because adding a restriction to a path is always safe, while the reverse needs a cast because it drops a promise. In practice const before the `*` is what you write constantly, especially on parameters, since `const char *s` tells callers the function only reads their buffer and lets them hand over a pointer they already hold as const. `char *const s` as a parameter promises the caller nothing, because the parameter is a private copy of the pointer; `T *const` earns its place on locals and on fixed addresses that must never be repointed, and because it can never be assigned it has to be initialized in its declaration.
<stdio.h>
int main(void)
{
int a = 1, b = 2;
const int *pc = &a; /* pointee read-only, pointer free */
int *const cp = &a; /* pointer fixed, pointee writable */
const int *const cc = &a; /* both locked */
pc = &b; /* fine: pc itself is not const */
*cp = 10; /* fine: what cp points at is not const */
/* *pc = 5; error: assignment of read-only location */
/* cp = &b; error: assignment of read-only variable 'cp' */
printf("a=%d b=%d\n", a, b);
printf("*pc=%d *cp=%d *cc=%d\n", *pc, *cp, *cc);
a = 42; /* a was never const, only the paths were */
printf("after a=42: *cc=%d\n", *cc);
return 0;
}
The `*` splits a declaration: const written before it protects the pointed-to data, const written after it protects the pointer variable.
Worked examples
const on a parameter widens what callers may pass
A read-only pointer parameter accepts both a writable array and a pointer that is already const.
<stdio.h>
static int count_char(const char *s, char c)
{
int n = 0;
while (*s != '\0') {
if (*s == c)
n++;
s++; /* s is not const, so it may walk */
}
return n;
}
int main(void)
{
char buf[] = "banana bread";
const char *lit = "banana bread";
printf("%d %d\n", count_char(buf, 'a'), count_char(lit, 'a'));
return 0;
}
Example explained
Line 1`const char *s` makes the characters unwritable through s but leaves s itself assignable, which is why `s++` compiles.
Line 2Passing `buf` works because `char *` converts implicitly to `const char *`; the function just accepts a stricter view of the same bytes.
Line 3Passing `lit` works only because the parameter is const; a `char *` parameter would discard a qualifier and require an explicit cast.
A typedef hides the star and moves the const
When the `*` is buried in a typedef, const can no longer reach the pointee and qualifies the pointer instead.
<stdio.h>
typedef char *string; /* the * is now part of the type name */
int main(void)
{
char text[] = "hi";
const string p = text; /* this is char *const p */
*p = 'H'; /* allowed: the characters are writable */
/* p = text; */ /* rejected: p is a const pointer */
printf("%s %s\n", text, p);
return 0;
}
Example explained
Line 1`typedef char *string;` folds the `*` into the name, so there is no longer a position between `char` and `*` for const to occupy.
Line 2`const string p` therefore qualifies the whole pointer type: p behaves as `char *const`, not as `const char *`.
Line 3`*p = 'H'` changes text[0], which is why both `text` and `p` print as "Hi"; the write went through a const pointer to non-const data.
Line 4Naming the type right to left ("p is a const pointer to char") predicts this; reading it left to right does not.
Important notes
Casting const away and writing is undefined only when the object itself was declared const; a plain `int` merely viewed through a `const int *` may still be modified legally by any other name for it.
Adding const one level deep converts implicitly (`int *` to `const int *`), but `int **` to `const int **` does not; C treats those as incompatible pointer types and needs an explicit cast.
Common mistakes
Reading `const int *p` as "p is constant": `*p = 0` is rejected but `p = &b` compiles, so the pointer quietly moves while you assume it is pinned.
Declaring `int *const p;` on one line and assigning p on the next: it fails to compile with "assignment of read-only variable", because a const pointer must be initialized where it is declared.
Casting the const off a string literal and writing, as in `*(char *)lit = 'H'`: this compiles cleanly and then crashes, since literals normally sit in write-protected memory.
Try it yourself
Change, predict, then run
Declare `char msg[] = "level";` and then three pointers to it: one that forbids writing the characters, one that cannot be repointed, and one that does both. Uncomment a write through each pointer in turn and note which line the compiler rejects and with what message.
Open the C workspaceCheck your understanding
Given `int x = 1, y = 2; const int *p = &x;`, what happens to the following two statements: `x = 9;` and `p = &y;`?
- Both compile: the const only forbids writes made through p, and it constrains neither x nor the value of p
- `x = 9;` fails, because pointing a const int * at x makes x read-only for the rest of the block
- `p = &y;` fails, because a pointer declared with const cannot be made to point somewhere else
- Both fail: `const int *p` locks the pointer and the object it currently points at
Show answer
The const in `const int *p` sits left of the `*`, so it restricts one access path: only `*p = ...` is rejected. x is an ordinary int and can still be assigned by its own name, and p is an ordinary pointer variable that can be repointed. Option 3 is the tempting one because it confuses this type with `int *const p`, where const sits right of the `*` and it is the assignment to p that fails instead.