🧬 Types & Qualifiers, In Depth
const: promises the compiler enforces
▶ Open the interactive lesson — free, no signupYou change a variable you never meant to touch, and three functions later the program prints garbage — now the evening disappears into hunting for the line that did it. const turns that whole category of bug into an instant compile error: one word, and the compiler refuses to let anything overwrite the value. It's also how a function can promise "I will only read your data" — a promise the compiler actually enforces.
C is famous for letting you do anything to memory. const is the opposite superpower: it lets you promise not to — and the compiler holds you to it. Slap const on a declaration and any attempt to assign through that name becomes a compile error. Bugs that would have been 2 a.m. debugging sessions become red squiggles.
#include <stdio.h>
int main(void) {
const double pi = 3.14159265358979;
double r = 2.0;
printf("area = %f\n", pi * r * r); /* reading is fine */
pi = 3.2; /* writing is NOT */
return 0;
}$ gcc promise.c promise.c: In function 'main': promise.c:8:8: error: assignment of read-only variable 'pi'
Be precise about what's promised: const means "not modified through this name". It doesn't necessarily put the object in read-only memory (though string literals and global const objects often do land in memory the operating system marks read-only). It's a contract checked at compile time, not a force field at runtime.
const + pointers: the four combinations
This is where 90% of the confusion lives, so let's kill it with one trick: read the declaration right-to-left.
| declaration | read right-to-left | can change *p? | can change p? |
|---|---|---|---|
int *p | p is a pointer to int | yes | yes |
const int *p | p is a pointer to an int that is const | no | yes |
int *const p | p is a const pointer to int | yes | no |
const int *const p | p is a const pointer to a const int | no | no |
The rule behind the trick: const qualifies whatever is immediately to its left (or, if it's the very first word, the thing to its right). So int const *p and const int *p mean exactly the same thing: the pointee is const. Only *const makes the pointer itself const.
▶ This spot has an interactive memgrid widget — open the interactive lesson to play with it.
🧠 Checkpoint: Which declaration lets you do p++ but forbids *p = 0?
int *const pconst int *pconst int *const pint *p
Show answer
const int *p — Read right-to-left: "p is a pointer to an int that is const". The pointee is protected, the pointer itself is an ordinary variable — so p++ is fine.
const in APIs: documentation that can't lie
The most valuable place for const is function parameters. size_t strlen(const char *s) tells you — with compiler enforcement — that strlen will only look at your string. A pointer-to-non-const parameter, like in strcpy's destination, tells you the function intends to write.
#include <stdio.h>
/* "I will only READ your data" — enforced by the compiler */
double average(const double *vals, int n) {
double sum = 0;
for (int i = 0; i < n; i++)
sum += vals[i]; /* vals[i] = 0; would not compile */
return sum / n;
}
int main(void) {
double temps[] = { 21.5, 23.0, 19.8, 22.4 };
printf("avg = %.2f\n", average(temps, 4));
return 0;
}Passing a char * where const char * is expected is fine (adding a promise is always safe). The reverse direction needs an explicit cast, because it drops a promise.
🧠 Checkpoint: You pass a plain char *name to a function taking const char *. What happens?
- Compile error — types differ
- It compiles: adding const is always a safe, implicit conversion
- It compiles but the string becomes permanently read-only
- Undefined behavior
Show answer
It compiles: adding const is always a safe, implicit conversion — Gaining a promise is free: T* converts implicitly to const T*. Only the other direction (losing const) needs an explicit cast. And no, the object itself is unchanged afterwards.
Casting const away — and when it explodes
C lets you strip const with a cast. Whether that's legal depends on the original object, not the pointer:
void sneaky(const int *p) {
int *w = (int *)p; /* cast away const... */
*w = 99; /* ...legal ONLY if the original
object was not const */
}
int main(void) {
int a = 1;
sneaky(&a); /* OK: a was never const */
const int b = 2;
sneaky(&b); /* UNDEFINED BEHAVIOR: b is const */
return 0;
}The rule: writing to an object that was defined const is undefined behavior — it may live in a read-only page and crash, or the compiler may have folded its value into the code already. Casting away const is only OK when the underlying object was never const to begin with (you just received it through a const pointer).
🧠 Checkpoint: When is writing through a cast-away-const pointer undefined behavior?
- Always — the cast itself is UB
- Never — the cast makes it legal
- When the pointed-to object was originally defined const
- Only if the object is a string literal
Show answer
When the pointed-to object was originally defined const — The cast is always legal; the WRITE is UB exactly when the underlying object was defined const (string literals count too — modifying them is UB for the same spirit of reason). If the object was mutable and merely viewed through a const pointer, writing is fine.
#define vs const vs enum for constants
#define MAX 100 | const int max = 100; | enum { MAX = 100 }; | |
|---|---|---|---|
| typed? | no — raw text paste | yes, real int | yes (int) |
| visible in debugger? | no | yes | yes |
| usable as array size / case label? | yes | not for case labels; array use makes a VLA in C17* | yes — it's a constant expression |
| scoped? | no — lives until #undef | yes, normal scope rules | yes |
*A quirk worth knowing: in C, a const int is not a "constant expression" (unlike C++). That's why enum is the classic idiom for integer constants that must appear in case labels or array sizes — though C23's constexpr finally fixes this properly.
▶ This spot has an interactive editor widget — open the interactive lesson to play with it.
Next: const's stranger sibling — a qualifier that tells the compiler less optimization, please: volatile.