const and volatileTwo qualifiers that act as contracts between your code and the compiler. const locks things down so the compiler catches mistakes. volatile forces the compiler to re-read memory every time, preventing optimisations that would break correctness. Together they let you balance safety with performance at a low level.
const Qualifierconst is a promise to the compiler โ and to other programmers โ that a value won't change. The compiler enforces it: any code that tries to modify a const object gets a compile error.
const int max_users = 100;
max_users = 200; // โ compile error
But const isn't just for simple variables. You can apply it to:
const int max_users)const with PointersThis is where most confusion happens. The position of const determines what is locked โ the pointer itself or the data it points at.
| Declaration | Data locked? | Pointer locked? | Read aloud (right โ left) |
|---|---|---|---|
const int *p; |
locked | free | "p is a pointer to a const int" |
int * const p; |
free | locked | "p is a const pointer to an int" |
const int * const p; |
locked | locked | "p is a const pointer to a const int" |
*p?const before * โ answer (2) is no (data locked).const after * โ answer (1) is no (pointer locked).
int value = 42;
int other = 99;
// case 1: const int *p โ data is read-only through p
const int *p = &value;
// *p = 100; // โ data locked
p = &other; // โ
pointer can move
// case 2: int *const q โ pointer is nailed in place
int *const q = &value;
*q = 100; // โ
data is writable
// q = &other; // โ pointer locked
const int *p, the data is read-only through p. But if the original variable value was not itself declared const, you can still modify it through the original name: value = 100;. const on a pointer restricts that access path only.
char const *p is identical to const char *p. Both mean "pointer to constant char." Pick one and stay consistent.
For a deeper dive with quizzes and walkthroughs, see Lesson 3 โ The const Matrix.
const in Function ParametersThis is where const earns its keep in everyday C. Marking a pointer parameter as const tells callers: "I will read your data, but I won't modify it."
void print_message(const char *msg) {
printf("%s\n", msg);
}
The compiler now enforces this โ any attempt to write through msg inside the function is a compile error. This is the standard convention for every C library function that takes a read-only string:
size_t strlen(const char *s); // reads only
int strcmp(const char *a, const char *b); // both read-only
char *strcpy(char *dest, const char *src); // dest writable, src read-only
const, output is not. If a pointer is input-only, mark it const. It catches bugs, enables compiler optimisations, and documents intent.
const with Return TypesYou can also mark a return value as const โ less common, but useful when you want to prevent the caller from modifying the returned data:
const char *get_error_message(void) {
return "Something went wrong";
}
Now the caller gets a const char * โ they can read the message but can't modify it.
const DemoHere's the book's example showing all the pieces together:
#include <stdio.h>
void show(const int *ptr) {
// *ptr = 10; // โ not allowed
printf("Value: %d\n", *ptr);
}
int main(void) {
int num = 5;
const int *p = #
int *const q = #
printf("num = %d\n", num);
// *p = 10; // โ cannot modify through const pointer
*q = 15; // โ
data modifiable through q
printf("num after q change = %d\n", num);
show(&num); // โ
function accepts const pointer
return 0;
}
Output:
num = 5
num after q change = 15
Value: 15
The key thing to notice: p and q point to the same num, but p can't modify it while q can. The restriction is on the pointer variable, not on the memory itself.
volatile Qualifiervolatile tells the compiler: this variable can change at any time, even if your code doesn't modify it. This prevents the compiler from optimising out reads and writes โ every access must go to real memory.
Without volatile, the compiler assumes a variable only changes when your code explicitly changes it. This lets it do clever optimisations โ caching values in registers, eliminating "unnecessary" reads, removing entire loops. But those optimisations break correctness when the value can change behind the compiler's back.
volatilevolatile int sensor_value;
while (sensor_value < 100) {
// wait for sensor to reach threshold
}
Without volatile, the compiler might optimise this loop away entirely โ it sees sensor_value never changes in the loop body, so it reads it once, compares to 100, and if it's below, loops forever (or eliminates the check entirely). With volatile, each iteration does a real memory read, so changes from external hardware are visible.
-O2This is Exercise 2 from the book. Write two programs โ one without volatile, one with โ and compare the generated assembly:
Without volatile โ at -O2, the compiler pre-computes the final value:
With volatile โ the loop stays, with real memory round-trips:
volatile does and does not do
volatile is not thread synchronisation. It does not make operations atomic. It does not guarantee ordering between different variables. It only guarantees: every read and write in your code actually happens, in the order you wrote them, to real memory.stdatomic.h or OS primitives (mutex, etc.).
const and volatileThese two look like they contradict each other โ const means "don't change it," volatile means "it might change at any time." But they target different actors:
| Qualifier | What it says | To whom? |
|---|---|---|
const | Your code must not modify this. | The compiler (enforcement + optimisation) |
volatile | Something else might modify this โ re-read from memory each time. | The compiler (optimisation blocker) |
They can coexist because they solve different problems. volatile says: "something outside this code (hardware, another thread) might change this value." const says: "your code must not change it."
const volatile int status_register = 0x1234;
This is routine in embedded systems โ a hardware status register that your code can read to check a flag, but writing to it would trigger hardware behaviour (or be silently ignored).
const volatile is not a contradiction: const blocks you, not the universe. The universe (hardware, another thread) can change it all it wants. You just can't.
const improves safety and clarity. It makes interfaces self-documenting and helps the compiler catch mistakes at build time instead of runtime.volatile preserves correctness in concurrent or hardware-driven systems where the compiler would otherwise optimise away essential reads and writes.Misuse or forgetfulness:
const โ risk accidental modification, lose compiler-enforced documentation, can't safely pass string literals.volatile โ risk the compiler removing critical reads/writes, causing bugs that only appear with optimisations enabled.These exercises are designed to be run as code โ each one is a short program that demonstrates a specific behaviour. Try each one in order.
const int through a pointer// ex01_const_through_pointer.c
#include <stdio.h>
int main(void) {
const int x = 10;
int *p = &x; // ๐ก compiler warning: discards const qualifier
*p = 20; // โ undefined behaviour โ modifying a const object
printf("%d\n", x);
return 0;
}
Compile with gcc -Wall -Wextra. What warnings do you get?
int *p = &x discards the const qualifier โ you're pointing a non-const pointer at const data. Even if the assignment *p = 20 appears to work (it might, or might not), this is undefined behaviour. The compiler is allowed to put x in read-only memory, and writing through p would crash.
volatile and assembly comparisonWrite two programs:
// ex02a_no_volatile.c
#include <stdio.h>
int main(void) {
int counter = 0;
for (int i = 0; i < 10000000; i++)
counter++;
printf("%d\n", counter);
return 0;
}
// ex02b_volatile.c
#include <stdio.h>
int main(void) {
volatile int counter = 0;
for (int i = 0; i < 10000000; i++)
counter++;
printf("%d\n", counter);
return 0;
}
Compile each with gcc -O2 -S and inspect the assembly:
gcc -O2 -S ex02a_no_volatile.c -o ex02a.s
gcc -O2 -S ex02b_volatile.c -o ex02b.s
The non-volatile version will likely have its entire loop eliminated โ the compiler pre-computes the final value and passes it directly to printf. The volatile version retains the full loop with real memory round-trips per iteration.
const char *msg function parameter#include <stdio.h>
void greet(const char *msg) {
msg[0] = 'X'; // โ try to modify
printf("%s\n", msg);
}
int main(void) {
greet("hello");
return 0;
}
Compile. The compiler should produce an error like "assignment of read-only location" or "discards qualifiers". Remove the offending line and compile again.
const int *p vs int *const p#include <stdio.h>
int main(void) {
int value = 42;
int other = 99;
// const int *p โ data is locked
const int *p = &value;
// *p = 100; // uncomment โ compile error?
p = &other; // allowed โ pointer can move
// int *const q โ pointer is locked
int *const q = &value;
*q = 100; // allowed โ data is writable
// q = &other; // uncomment โ compile error?
// const int *const r โ both locked
const int *const r = &value;
// *r = 200; // uncomment โ compile error?
// r = &other; // uncomment โ compile error?
return 0;
}
Uncomment each line one at a time. Which compile? Which don't?
const volatile int flag#include <stdio.h>
const volatile int flag = 0x1234;
int main(void) {
for (int i = 0; i < 5; i++)
printf("flag = %d\n", flag);
// flag = 99; // uncomment โ compile error?
return 0;
}
Two facts in one program: volatile forces a memory read each loop iteration (though the value doesn't change here, the compiler can't cache it). const prevents you from writing to flag.
const int *p lock?*p is read-only. The pointer itself can be reassigned. Read: "p is a pointer to a constant int."
int * const p lock?p cannot point elsewhere. The data it points at is writable. Read: "p is a constant pointer to an int."
const and volatile?const prevents your code from modifying a variable. volatile prevents the compiler from optimising away reads/writes, because something outside your code might change the value.
const and volatile coexist on the same variable?const says you can't write. volatile says the compiler must re-read every time. Something else (hardware, another thread) can change the value โ you just can't.
const pointer declaration and immediately know what's locked โ using the two-question test.const char * on function parameters as a compiler-enforced seatbelt for read-only input.volatile does and when it's necessary (hardware, signal handlers, any value that changes outside your code).const volatile is not a contradiction โ const locks your writes, volatile forces re-reads because something else might write.gcc -O2 -S) to see the difference volatile makes.volatile doesn't make things atomic, how const interacts with malloc, when you'd use const volatile in practice โ type it and we'll dig in.