Question

Why `volatile` Is Needed in C: Meaning, Uses, and Examples

cdeclarationvolatile

Question

In C, why is the volatile keyword needed? What is it used for, and what effect does it have on a program?

For example, in what situations should a variable be declared as volatile, and what changes when the compiler sees that qualifier?

Short Answer

By the end of this page, you will understand what volatile means in C, why it exists, and when it should be used. You will also learn what volatile does not guarantee, how it affects compiler optimizations, and how it is commonly used with hardware registers, signal handlers, and shared state changed outside normal program flow.

Concept

volatile in C tells the compiler that the value of an object may change in ways the compiler cannot predict.

Normally, the compiler is allowed to optimize code by assuming that if your program did not write to a variable, then its value has not changed. That assumption is often correct for ordinary variables.

However, some values can change outside the current code path, such as:

  • memory-mapped hardware registers
  • variables modified by a signal handler
  • values updated by external devices or special runtime environments

When an object is declared volatile, the compiler must be more careful:

  • it should perform actual reads from memory when the program asks for the value
  • it should perform actual writes to memory when the program assigns to it
  • it should avoid certain optimizations that would remove, combine, or cache those accesses

Why this matters

Without volatile, the compiler may:

  • keep a value in a register instead of re-reading memory
  • remove a loop that appears to wait for a value to change
  • combine multiple reads or writes into fewer operations

That can break programs that interact with hardware or asynchronous events.

What volatile does not do

A very important point: volatile is not a threading or synchronization tool.

It does not guarantee:

  • atomic reads or writes
  • thread safety
  • mutual exclusion
  • memory ordering between threads in the modern concurrency sense

For multithreaded communication, C uses atomics from <stdatomic.h> or platform-specific synchronization tools such as mutexes.

So the core idea is:

  • use volatile when a value can change for reasons the compiler cannot see
  • do not use it as a replacement for proper synchronization

Mental Model

Think of a normal variable like a note on your desk.

If you look at it once, you might keep the value in your head and stop checking the paper. That is like the compiler caching a value in a register.

A volatile variable is like a display board that someone else may update at any time.

Because the value may change unexpectedly, you must look at the board each time you need the latest value. You cannot safely assume the old value is still correct.

That is what volatile tells the compiler: do not assume this value stays the same just because this code did not change it.

Take Quiz

Syntax and Examples

Basic syntax

volatile int flag;

This means flag is an integer whose value may change unexpectedly.

Example: waiting for a hardware or external update

volatile int ready = 0;

void wait_until_ready(void) {
    while (ready == 0) {
        /* wait */
    }
}

Why volatile matters here

If ready were not volatile, the compiler might read it once, decide it is still 0, and turn the loop into something that never checks memory again.

With volatile, the compiler must re-read ready each time through the loop.

Example: memory-mapped hardware register

#define STATUS_REG (*(volatile unsigned int *)0x40000000)

void wait_for_device(void) {
    while ((STATUS_REG & 1u) == 0u) {
        /* wait until device sets bit 0 */
    }
}

Here, the value at address 0x40000000 is assumed to be a hardware register. The hardware may change it independently of the CPU's normal program logic.

volatile prevents the compiler from treating reads from that address as ordinary memory accesses.

Example: signal handler

#include <signal.h>
#include <stdio.h>

volatile sig_atomic_t stop = 0;

void handle_sigint(int sig) {
    stop = 1;
}

int main(void) {
    signal(SIGINT, handle_sigint);

    while (!stop) {
        /* do work */
    }

    printf("Stopping...\n");
    return 0;
}

A signal handler can interrupt normal execution and change stop. Declaring it volatile sig_atomic_t is the traditional safe pattern for this case.

Important note

volatile changes how the compiler treats accesses. It does not mean the variable is stored in RAM forever, and it does not make code automatically correct in concurrent programs.

Step by Step Execution

Consider this code:

volatile int ready = 0;

void wait_until_ready(void) {
    while (ready == 0) {
    }
}

Now walk through it:

  1. ready is declared as volatile int.
  2. The compiler sees that ready may change unexpectedly.
  3. wait_until_ready() starts running.
  4. The loop checks ready == 0.
  5. Because ready is volatile, the compiler performs a real memory read.
  6. If the value is still 0, the loop repeats.
  7. On the next iteration, the compiler must read ready again.
  8. If some external event changes ready to 1, a later loop iteration sees the new value.
  9. The condition becomes false, and the function exits the loop.

What could happen without volatile

If ready were a normal int, the compiler might reason:

  • this code never changes ready
  • therefore ready must stay 0
  • therefore repeated reads are unnecessary

That could lead to code that effectively behaves like this idea:

int temp = ready;
while (temp == 0) {
}

Now the loop never sees updates made externally.

The exact generated machine code depends on the compiler and optimization level, but this is the kind of optimization volatile is meant to prevent.

Real World Use Cases

1. Memory-mapped device registers

Embedded systems often access hardware by reading and writing fixed memory addresses.

#define UART_STATUS (*(volatile unsigned int *)0x40001000)
#define UART_DATA   (*(volatile unsigned int *)0x40001004)

These values can change because of the device, not because of ordinary C code.

2. Polling for hardware state changes

A program may wait until a peripheral finishes work:

while ((UART_STATUS & 0x01u) == 0u) {
}

Without volatile, the compiler might optimize the repeated reads incorrectly.

3. Signal handlers

A signal can interrupt execution and modify a flag that the main code checks.

volatile sig_atomic_t interrupted = 0;

4. Special runtime or low-level systems code

Operating systems, firmware, kernels, and bootloaders often work with registers or memory locations that can change externally.

5. Debug or instrumentation hooks

In rare low-level cases, developers use volatile to force specific reads and writes to happen because the value may be observed outside normal program assumptions.

This should be done carefully and only when the reason is clear.

Take Quiz

Real Codebase Usage

In real codebases, volatile is usually seen in low-level code, not ordinary business logic.

Common patterns

Hardware register definitions

#define REG32(addr) (*(volatile unsigned int *)(addr))

This pattern appears in embedded code to make register accesses explicit.

Polling loops with timeout protection

int wait_ready_with_timeout(void) {
    int count = 1000000;
    while ((status_reg & 1u) == 0u) {
        if (--count == 0) {
            return -1;
        }
    }
    return 0;
}

Developers often combine volatile reads with a timeout so the program does not get stuck forever.

Signal-safe flags

volatile sig_atomic_t stop_requested = 0;

This is a common way to let a signal handler notify the main loop.

What developers usually do instead in application code

In modern application code, especially multithreaded programs, developers prefer:

  • _Atomic or <stdatomic.h> for shared state
  • mutexes and condition variables
  • message queues or event loops

That is because volatile is about compiler optimization of accesses, not safe synchronization.

Guard clause mindset

When working with volatile hardware or external state, code often uses guard clauses and validation:

if ((STATUS_REG & ERROR_BIT) != 0u) {
    return DEVICE_ERROR;
}

This keeps low-level state checks clear and predictable.

Common Mistakes

Mistake 1: Using volatile for thread safety

Broken idea:

volatile int counter = 0;

/* Thread A */
counter++;

/* Thread B */
counter++;

Why this is wrong:

  • counter++ is not atomic
  • both threads can interfere with each other
  • updates can be lost

Use atomics or a mutex instead.

Mistake 2: Thinking volatile prevents all optimization

volatile only restricts optimization of accesses to that specific object. It does not disable optimization for the whole function or program.

Mistake 3: Forgetting it for hardware registers

Broken example:

#define STATUS_REG (*(unsigned int *)0x40000000)

while ((STATUS_REG & 1u) == 0u) {
}

This may be optimized in unsafe ways because the compiler sees an ordinary pointer dereference.

Correct version:

#define STATUS_REG (*(volatile unsigned int *)0x40000000)

Mistake 4: Overusing volatile

Some beginners add volatile to many variables "just in case." That usually makes code slower and less clear.

Use it only when the value may change outside the compiler's visible control flow.

Mistake 5: Confusing volatile with const

const int x = 5;
volatile int y;
  • const means your code should not modify the object through that name
  • volatile means the object may change unexpectedly

A variable can even be both:

const volatile unsigned int *reg;

This can represent a read-only hardware register whose value changes externally.

Mistake 6: Ignoring sig_atomic_t for signals

For signal handlers, volatile alone is not the traditional safe choice. Use:

volatile sig_atomic_t flag;

This is intended for communication between normal code and a signal handler.

Comparisons

ConceptWhat it meansGood forNot enough for
volatileValue may change unexpectedly; force actual accessesHardware registers, signal flags, low-level pollingThread safety, atomicity
constDo not modify through this nameRead-only values, APIs, safetyExternal changes, synchronization
_Atomic / <stdatomic.h>Atomic operations and memory orderingMultithreaded shared dataDirect replacement for hardware register semantics in every case
ordinary variableNormal compiler assumptions applyRegular program stateExternal asynchronous changes

volatile vs _Atomic

volatile int flag;
_Atomic int counter;
  • volatile int flag means: always read/write the object as written in code
  • _Atomic int counter means: operations can be made atomic and synchronized between threads

If your problem is hardware or signals, volatile may be correct.

If your problem is multiple threads, atomics are usually the right tool.

volatile vs const

These qualifiers solve different problems:

  • const: your code should not write to it
  • volatile: the value may change unexpectedly

You may see both together in embedded code:

const volatile unsigned int *status = (const volatile unsigned int *)0x40000000;

This suggests the program should only read from the register, but the register's value may still change due to hardware.

Take Quiz

Cheat Sheet

Quick definition

volatile tells the C compiler that an object's value may change unexpectedly, so accesses must not be optimized away or cached as if the value were stable.

Syntax

volatile int x;
int volatile x;

These mean the same thing.

Typical uses

  • memory-mapped hardware registers
  • variables changed by signal handlers
  • low-level polling of external state

Not for

  • thread synchronization
  • atomic increment/decrement
  • locking
  • replacing mutexes or atomics

Common examples

volatile sig_atomic_t stop;
#define REG (*(volatile unsigned int *)0x40000000)

Key rule

Use volatile when the value can change for reasons the compiler cannot see in normal code flow.

Related qualifiers

  • const: cannot be modified through that name
  • volatile: may change unexpectedly
  • _Atomic: supports atomic and synchronized access

Edge case reminder

A volatile access does not automatically make a multi-step operation safe:

volatile int x;
x++;   /* still not atomic */

FAQ

What does volatile do in C?

It tells the compiler that a variable may change unexpectedly, so reads and writes to it should happen as written instead of being optimized away.

Why is volatile used for hardware registers?

Because hardware can change register values independently of normal C code. The compiler must not assume the value stays the same.

Is volatile needed for multithreading?

Usually no. volatile does not provide atomicity or proper synchronization between threads. Use atomics or locks instead.

Does volatile make code slower?

It can, because the compiler has fewer optimization options for that object. That is why it should be used only where needed.

Can a variable be both const and volatile?

Yes. That often represents a read-only value from the program's point of view that may still change externally, such as a status register.

Does volatile guarantee fresh values across CPU cores?

No. It is not a full memory-ordering or synchronization mechanism for concurrent programs.

When should I use volatile sig_atomic_t?

Use it for a flag shared between normal code and a signal handler, which is a classic C pattern.

Can volatile fix an infinite polling loop?

It can help if the loop is waiting on a value changed externally and the problem is compiler optimization of repeated reads. But you still need correct program design.

Take Quiz

Mini Project

Description

Build a small C program that simulates waiting for an external event using a signal. This demonstrates why a value that changes outside normal program flow should be declared carefully. It is a practical way to see the classic volatile sig_atomic_t pattern in action.

Goal

Create a program that runs in a loop until the user sends Ctrl+C, then exits cleanly using a flag set by a signal handler.

Requirements

  • Declare a stop flag that can be safely checked in normal code and set in a signal handler.
  • Register a handler for SIGINT.
  • Keep looping until the stop flag changes.
  • Print a message when the program starts and when it exits.
Take Quiz

Keep learning

Related questions

View more