Embedded C
const Correctness
const looks like a small keyword, but it's a real, compiler-enforced promise — "this won't
be modified through this name" — checked at compile time, not just a comment for other humans. On a
microcontroller, that promise also has a physical consequence: const data can often live in
flash instead of RAM, which on a device with far more flash than RAM is a genuinely significant space
saving.
- What it is
- A type qualifier meaning "this value is not to be modified through this particular name" — enforced by the compiler as a real error, not a suggestion.
- Mental model
- A promise you make to the compiler, which it then checks on every line for you — the earliest, cheapest possible place to catch an accidental write to something that shouldn't change.
- Key numbers
- Two independent things a pointer can be const about: the pointer itself, and what it points to — four total combinations, covered below.
- Reach for it when
- A value genuinely never changes after initialization, or a function parameter is read-only and callers should be free to pass read-only data to it.
const on an ordinary variable
const int MAX_RETRIES = 3;
MAX_RETRIES = 5; // COMPILE ERROR — assignment of read-only variable
Unlike a preprocessor #define,
a const variable is a real, typed value the compiler is fully aware of — it can be
type-checked, it has an actual address you can take with &, and it shows up properly in
a debugger by name.
const and pointers — four combinations
A pointer has two separate things that can independently be const: the pointer's own value
(which address it holds), and the data it points to. This is genuinely where most beginners' confusion
with const comes from — there are four distinct combinations:
| Declaration | Meaning |
|---|---|
int *p | Neither is const — can reassign p, can modify *p. |
const int *p | Pointer to const — can reassign p to point elsewhere, cannot modify *p through it. |
int *const p | Const pointer — cannot reassign p, can freely modify *p. |
const int *const p | Both — cannot reassign p, cannot modify *p either. |
int x = 5, y = 10;
const int *p1 = &x;
p1 = &y; // OK — p1 itself isn't const
// *p1 = 20; // ERROR — the pointed-to int is const through p1
int *const p2 = &x;
*p2 = 20; // OK — the int isn't const
// p2 = &y; // ERROR — p2 itself is const, can't be reassigned
Reading a const pointer declaration
The reliable trick: read the declaration right to left starting from the variable name.
const int *p1 reads as "p1 is a pointer to an int that is
const" — the const applies to whatever is immediately to its right (here, effectively the
int, since const int and int const mean the same thing).
int *const p2 reads as "p2 is a const pointer to an int" — the
const applies to p2 itself, immediately to its left this time.
Why const data can live in flash
A microcontroller's flash storage is typically far larger than its RAM, and flash is non-volatile — see
ROM & Storage for that general distinction. Data
declared const and initialized at compile time never needs to be writable, so many toolchains
place it directly in flash instead of copying it into RAM at startup, freeing that RAM entirely:
const char version_string[] = "v2.4.1"; // can live entirely in flash
const uint16_t lookup_table[256] = { ... }; // a large table costing zero RAM
This is exactly the memory-hierarchy tradeoff introduced in
Storage Classes & Scope's memory
map — const global data typically lands in .rodata, distinct from
.data's writable, RAM-resident globals.
Real-world applications
- Lookup tables — a large precomputed table (a sine table, a CRC table, a font bitmap) declared
constcosts flash instead of scarce RAM. - Read-only function parameters — a function that only reads a buffer, declared
void process(const uint8_t *data, size_t len), documents and enforces "this function won't modify your data" at the call site. - Firmware version strings and configuration defaults — fixed at compile time, never meant to change at runtime, and a natural fit for flash-resident
conststorage.
Gotchas
- Casting away
const((int*)p1on aconst int *) compiles, but then modifying the underlying value is undefined behavior if the original object was genuinely declaredconst— the cast defeats the compiler's check, it doesn't change reality. - Placing
constdata in flash means it genuinely cannot be modified at runtime, even if a bug somehow tried — on some architectures, attempting to write to flash-mapped memory directly faults, which can turn a logic bug into a hard crash instead of silent corruption. - A function parameter declared as a plain (non-const) pointer signals to every caller "this function might modify what you pass in" — even when it doesn't. Marking read-only parameters
constis genuinely valuable self-documentation, not just style preference. - Whether
constdata actually ends up in flash instead of RAM is a toolchain and linker-script decision, not guaranteed by the C standard — always verify with your specific build's.mapfile (see Compilation & Linking) rather than assuming.