Lesson 8 ยท ~30 minutes ยท ties to mission: understand how the machine works under the hood

const and volatile

Two 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.

Based on A Little Book of C, Chapter 27 (pp. 89โ€“92)

The const Qualifier

const 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 with Pointers

This is where most confusion happens. The position of const determines what is locked โ€” the pointer itself or the data it points at.

DeclarationData 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"
The two-question test (1) Can I reassign the pointer?
(2) Can I write through *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
Important nuance With 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.
Note: 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 Parameters

This 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
Rule of thumb In function parameters: input is 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 Types

You 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.

Tiny Code: const Demo

Here'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 = &num;
    int *const q = &num;

    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.

The volatile Qualifier

volatile 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.

When to use volatile

Example: polling a sensor

volatile 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.

See it for yourself: with and without -O2

This 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:

;; ex21.s โ€” int counter; no volatile mov esi, 10000000 ; pre-computed final value call printf ; the entire loop is GONE

With volatile โ€” the loop stays, with real memory round-trips:

;; ex22.s โ€” volatile int counter .L3: mov eax, DWORD PTR counter[rip] ; real read from memory inc eax mov DWORD PTR counter[rip], eax ; real write to memory cmp eax, 9999999 jle .L3
What 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.

For atomicity or thread safety, you need stdatomic.h or OS primitives (mutex, etc.).

Combining const and volatile

These 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:

QualifierWhat it saysTo whom?
constYour code must not modify this.The compiler (enforcement + optimisation)
volatileSomething 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).

Memory for the model 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.

Why It Matters

Misuse or forgetfulness:

Try It Yourself โ€” Exercises from the Book (p. 93)

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.

Exercise 1 โ€” Modifying a 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?

๐Ÿ’ก What's happening here?

Show explanation The compiler warns because 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.

Exercise 2 โ€” volatile and assembly comparison

Write 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.

Exercise 3 โ€” 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.

Exercise 4 โ€” 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?

Exercise 5 โ€” 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.

Check yourself

โ‘  What does const int *p lock?

Show answer The data โ€” *p is read-only. The pointer itself can be reassigned. Read: "p is a pointer to a constant int."

โ‘ก What does int * const p lock?

Show answer The pointer โ€” p cannot point elsewhere. The data it points at is writable. Read: "p is a constant pointer to an int."

โ‘ข What is the difference between const and volatile?

Show answer 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.

โ‘ฃ How can const and volatile coexist on the same variable?

Show answer Because they restrict different actors. 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.

What you can now do

After this lesson
Questions? Ask your agent. Anything murky โ€” why 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.
Sources & further reading
ยท A Little Book of C, Chapter 27 (pp. 89โ€“92) โ€” const, volatile, and the "Try It Yourself" exercises.
ยท Lesson 3 โ€” The const Matrix โ€” the full const-on-pointers taxonomy with quizzes.
ยท Beej's Guide โ€” const pointers
ยท cppreference โ€” const qualifier ยท cppreference โ€” volatile
ยท LLVM Blog: What Every C Programmer Should Know About Undefined Behavior (#2 discusses volatile)