Learn C Programming

Lesson 2 of 7 · Constants, Qualifiers, Input and Output

Module 3 · Constants, Qualifiers, Input and Output

const and volatile: Promises You Make to the Compiler

FreeReading

In this lesson

  • Put const on a value and say exactly what the compiler then refuses to do.
  • Explain volatile in one sentence, and recognise the kind of program that needs it.
  • Name static, extern and register as storage classes, which are a different idea.

Kenji writes software for a machine that stops when a button is pressed. His program waits by reading a flag that the button's circuit writes into memory. It worked for a year, he turned the optimiser on, and the machine stopped stopping.

Nothing about the flag changed. What changed is that the compiler noticed nobody in the program ever writes to it, so it read it once and reused the answer forever. This lesson is about the two words you use to tell the compiler things it cannot work out by reading your code.

const is a promise, and the compiler holds you to it

A qualifier is an extra word beside a type. It does not change the size or the values the variable can hold. It changes what the compiler will let you do with it, and what it is allowed to assume.

const says one thing: nothing in this program assigns to this variable after it is born. Break that promise and the build stops.

Where a qualifier goes

const int limit = 60;        the usual order
int const limit = 60;        the same thing, legal, rare in practice
const double PI = 3.14159;   any type takes it

volatile int sensor;         the other qualifier this lesson covers
const volatile int clock;    both at once, and this really happens
  • The order of a qualifier and a type is free. Everyone writes the qualifier first, so write it first.
  • A const has to be given its value where it is declared, because there is no later chance.
  • restrict is the third qualifier. It is a promise about pointers, and Module 11 is where it can be explained honestly.
#include <stdio.h>

int main(void)
{
    const int SEATS_PER_ROW = 8;
    const int ROWS = 12;
    int booked = 71;

    printf("capacity : %d\n", SEATS_PER_ROW * ROWS);
    printf("booked   : %d\n", booked);
    printf("free     : %d\n", SEATS_PER_ROW * ROWS - booked);
    return 0;
}
capacity : 96
booked   : 71
free     : 25

Add one line, SEATS_PER_ROW = 10;, anywhere below the declaration. The Playground's GCC 12 answers error: assignment of read-only variable 'SEATS_PER_ROW' and no program is produced.

That is the entire mechanism. There is no run time check and no cost: the refusal happens while you are still typing, and the finished program contains nothing extra.

What const does not do

Three things people expect from const that it does not give you, and knowing them early saves an afternoon each.

It does not make a compile time constant. A const int is a variable you promised not to change. It is not a constant expression, so it cannot size a fixed array, and using it that way gives you a variable length array instead. Module 9 covers what that means.

It does not protect the value from everything. A pointer can be pointed at a const variable with the const cast away, and writing through that pointer compiles. What happens then is undefined behaviour, and the brain teaser at the foot of this lesson shows it happening.

It does not make anything safe across threads. A value that is never written is safe to read from anywhere, but that is a property of never writing it, not of the word const.

const and a pointer: two positions, two meanings

Module 11 teaches pointers properly. One thing belongs here. It is the single most misread line in C, and you will meet it in a library header long before Module 11.

A pointer declaration has two things that could be constant: the thing pointed at, and the pointer itself. Where you put the word decides which.

DeclarationCan you change the value it points atCan you point it somewhere else
int *p;yesyes
const int *p;noyes
int const *p;no, the same as the row aboveyes
int *const p;yesno
const int *const p;nono

The reading trick is to say the declaration backwards from the name. int *const p reads as: p is a const pointer to int.

Both mistakes have their own message. Writing through the first kind gives error: assignment of read-only location '*p', and repointing the second kind gives error: assignment of read-only variable 'q'. Module 11 lesson 5 comes back to this with the memory drawn.

volatile: the variable that changes behind your back

Every optimisation a compiler performs rests on one assumption: it can see everything that touches your variables. For most programs that is true.

It is false in three places, and they are all real. A memory address wired to a piece of hardware. A variable a signal handler writes. A variable another thread writes.

volatile says this variable can change without any code of yours changing it. So read it from memory every single time, and never cache or delete a read.

You cannot see that from inside the program, because the value is the same either way. You can see it in the machine code the compiler produces.

The compiler reads a plain variable as few times as it likes

Two functions, identical except for one word. Each reads its variable twice and adds the two readings.

#include <stdio.h>

volatile int sensor = 21;
int counter = 21;

int twice_volatile(void)
{
    int first = sensor;
    int second = sensor;
    return first + second;
}

int twice_plain(void)
{
    int first = counter;
    int second = counter;
    return first + second;
}

int main(void)
{
    printf("twice_volatile : %d\n", twice_volatile());
    printf("twice_plain    : %d\n", twice_plain());
    return 0;
}
twice_volatile : 42
twice_plain    : 42

Both answers are 42, and they always will be. The difference is invisible from inside the program, so look at the machine code instead.

Compiled with the Playground's optimisation level, gcc -O2 -S, the two functions come out like this. The lines that matter are the ones reading memory.

twice_volatile:
        movl    sensor(%rip), %eax
        movl    sensor(%rip), %edx
        addl    %edx, %eax
        ret
twice_plain:
        movl    counter(%rip), %eax
        addl    %eax, %eax
        ret

Read the two blocks side by side. The volatile one loads from memory twice, exactly as the source asked. The plain one loads once and doubles what it got.

That second version is correct for an ordinary variable, and it is Kenji's bug. Both readings had to happen, and the compiler had no way to know that.

Compile the same file at -O0 and both functions load twice, which is why Kenji's program worked until the day he turned the optimiser on.

So volatile does not change the value, the type or the size. It removes one permission the compiler otherwise has.

Storage classes are a different word entirely

Beside a type you may also see static, extern, auto or register. Those are storage classes, not qualifiers, and they answer a different question.

A qualifier answers "what may be done to this value". A storage class answers "where does this variable live, and for how long".

WordKindWhat it saysTaught in
constqualifiernothing here assigns to itthis lesson
volatilequalifiersomething outside may change itthis lesson
restrictqualifierno other pointer reaches this memoryModule 11
staticstorage classkeeps its value between callsModule 7
externstorage classdefined in another fileModule 7
registerstorage classa hint that modern compilers ignoreModule 7

They can appear together, and static const int limit = 60; is an ordinary thing to write. Module 7 gives the right hand column its own lesson.

Example 1: the smallest const program

Two named values, both used, neither changed. This is what you will write nine times out of ten.

#include <stdio.h>

int main(void)
{
    const int MINUTES_PER_HOUR = 60;
    int hours = 7;

    printf("%d hours is %d minutes\n", hours, hours * MINUTES_PER_HOUR);
    return 0;
}
7 hours is 420 minutes

Change hours freely. Try to change MINUTES_PER_HOUR and the compiler stops you by name.

Run in Compiler
Example 2: a settings block, which is where const earns its place

Every value a program is configured with, in one place, at the top, typed and named.

#include <stdio.h>

const int MAX_UPLOAD_MB = 25;
const int SESSION_MINUTES = 15;
const int RETRY_LIMIT = 3;
const double VAT_RATE = 0.15;

int main(void)
{
    printf("upload limit    : %d MB\n", MAX_UPLOAD_MB);
    printf("session length  : %d minutes\n", SESSION_MINUTES);
    printf("retries allowed : %d\n", RETRY_LIMIT);
    printf("VAT rate        : %.2f\n", VAT_RATE);
    printf("session seconds : %d\n", SESSION_MINUTES * 60);
    return 0;
}
upload limit    : 25 MB
session length  : 15 minutes
retries allowed : 3
VAT rate        : 0.15
session seconds : 900

The last line has a magic number in it. Sixty is the number of seconds in a minute, which will not change, and naming it would be noise. Judgement, not a rule.

Run in Compiler
Example 3: volatile written the way firmware writes it

This is the shape of Kenji's program, with the waiting removed so that it finishes. The flag is volatile, so both reads happen.

#include <stdio.h>

/* Something outside this program can write to stop_requested: a button,
   a signal handler, another thread. Nothing in this file writes to it. */
volatile int stop_requested = 0;

int main(void)
{
    int first_look = stop_requested;
    int second_look = stop_requested;

    printf("first look  : %d\n", first_look);
    printf("second look : %d\n", second_look);
    printf("both agree  : %d\n", first_look == second_look);
    return 0;
}
first look  : 0
second look : 0
both agree  : 1

Nothing is pressing a button here, so both looks agree. On Kenji's machine they would not. The whole point of the word is that the compiler cannot tell the two situations apart.

Run in Compiler

Where this is used

  • The Linux kernel's device drivers. A memory mapped register is declared volatile, because reading the same address twice really does give two different answers. That is the original reason the word exists.
  • Signal handlers in any C program. The standard says a handler may safely touch a variable of type volatile sig_atomic_t and almost nothing else. Press Ctrl and C in a terminal and this is the mechanism that lets the program notice.
  • Every standard library header you include. <string.h> declares strlen as taking a const char *. That const is a promise to you, in writing, that the function will not modify your text.
  • SQLite's configuration table. SQLite keeps its compile time settings in a const structure that the whole library reads and nothing writes. It can then live in the read only part of the program.

Common mistakes

1. Assigning to a const.

const double VAT_RATE = 0.15;
VAT_RATE = 0.20;

GCC 12 says error: assignment of read-only variable 'VAT_RATE' and stops. This is the good case: the mistake is caught at the earliest possible moment and named precisely. If the rate really does change while the program runs, it was never a constant and it wants an ordinary variable.

2. Declaring a const and filling it in later.

const int limit;
limit = 60;

GCC 12 says error: assignment of read-only variable 'limit', and points at the second line rather than the first. A const gets its value at birth or never, because the only line that could give it one is the line the compiler refuses.

3. Putting the const on the wrong side of a pointer.

int a = 1;
int b = 2;
int *const q = &a;
q = &b;

GCC 12 says error: assignment of read-only variable 'q'. The pointer was frozen, not what it points at, and the writer almost always meant the other one. Read it backwards from the name, and check it against the table above.

4. Expecting volatile to make something thread safe.

volatile int counter = 0;
counter = counter + 1;

No message at either command line, and the program builds. That line is a read, an add and a write, and another thread can land between any two of them. volatile stops the compiler caching a value; it promises nothing about two things happening at once. The tools for that are in <stdatomic.h> and are outside this track.

Brain teaser

Zara wants to prove that const is a promise rather than a lock, so she breaks it on purpose. Casting is Module 4, and (int *) here just tells the compiler to treat the address as an ordinary one.

#include <stdio.h>

int main(void)
{
    const int limit = 60;
    int *escape = (int *)&limit;

    *escape = 30;

    printf("limit reads as %d\n", limit);
    return 0;
}

Nothing is reported, at either command line, not even a local gcc -Wall -Wextra. Compiled with the Playground's -O2 it printed limit reads as 60. Compiled at -O0, the same source printed limit reads as 30.

Say why one program can honestly print two different answers, and which of the two, if either, is the wrong one.

Then the harder half. Change const int limit to const volatile int limit and predict what each optimisation level prints.

The compiler was told the value never changes. Ask what it is then entitled to do with the printf, rather than what it does to the memory.

Exercise 1: seat-planEasy

Kenji's exam hall has 12 rows per block and 8 seats per row. Read the number of blocks and the number of booked seats, and print the three totals.

Input. One line with two integers: the number of blocks, then the seats already booked.

Output. Three lines: rows, capacity and free, each followed by one space and its number.

Constraints. 0 <= blocks <= 10000, 0 <= booked <= capacity. The numbers 12 and 8 appear once each, as named constants.

Sample. Input 3 71 gives rows 36, capacity 288, free 217.

#include <stdio.h>

int main(void)
{
    const int SEATS_PER_ROW = 8;
    const int ROWS_PER_BLOCK = 12;

    int blocks = 0;
    int booked = 0;
    scanf("%d %d", &blocks, &booked);

    /* Three printf calls. Every number in them comes from a name. */

    return 0;
}

Graded as seat-plan. The judge sees your output and not your constants, so the last rule is on your honour. Write it the other way once and count the edits a rebuilt hall would cost.

Run in Compiler
Exercise 2: name every numberMedium

Here is a working program with six bare numbers in it. Rewrite it so that every value which never changes has a name, and the output is byte for byte what it is now.

#include <stdio.h>

int main(void)
{
    int students = 45;

    printf("exam minutes : %d\n", 90);
    printf("pass mark    : %d\n", 40 * 60 / 100);
    printf("papers       : %d\n", students * 3);
    printf("bundles      : %d\n", students * 3 / 25);
    return 0;
}
exam minutes : 90
pass mark    : 24
papers       : 135
bundles      : 5

Not graded in this module, because a judge reads output and cannot see a const. Do it anyway, then change the pass fraction from 40 to 50 and count how many lines you had to touch in each version.

Run in Compiler

Common doubts

  • Should I put const on everything that does not change?

    On values, yes, and it costs nothing. The habit pays twice: the compiler catches an accidental assignment, and a reader knows which values are settings and which are working numbers.

  • Will I ever write volatile?

    Only if you write firmware, a signal handler or an operating system. It is here because you will read it in other people's code, and because misusing it is common.

  • Is const int different from int const?

    No, they are the same declaration. The second form reads better beside a pointer, which is the only reason anyone writes it.

  • Does volatile make my program slower?

    A little, and only where it is used. Every read really happens, so the compiler cannot keep the value in a register, which is the whole point.

  • Why does the compiler let me cast const away at all?

    Because C trusts you, and because older libraries have functions that take a plain pointer and do not modify it. The cast is the escape hatch, and the teaser shows what it costs.

Key takeaways

  • A qualifier changes what may be done to a value, never its size or its range.
  • const refuses assignment at compile time, by name, and costs nothing at run time.
  • A const int is not a constant expression, so it does not size a fixed array in C.
  • With a pointer, the side the const sits on decides whether the target or the pointer is frozen.
  • volatile tells the compiler to read memory every time, because something outside the program may write it.
  • static, extern and register are storage classes, a different question, taught in Module 7.

Next comes the other half of this module. It starts with the one function you have used in every program so far and never actually read.

End of lesson 2

Mark it done, and your progress moves with you.

Next: printf: Format Specifiers, Width and Precision