C - Const and Volatile Qualifiers in C

In C programming, const and volatile are type qualifiers used to tell the compiler how a variable should be accessed or modified. They do not create new data types; instead, they provide additional information about how an existing data type should be treated.

Understanding these qualifiers is especially important in embedded systems, operating systems, device drivers, hardware programming, and multi-module applications.

1. The const Qualifier

The const keyword is used when you want to indicate that the value of an object should not be modified through a particular identifier.

For example:

const int age = 25;

Here, age is declared as a constant integer. After initialization, you cannot normally assign a new value to it:

age = 30;   // Error

The compiler will generally report an error because age is declared as const.

2. Why Use const?

Using const provides several advantages.

First, it prevents accidental modification of values that should remain unchanged. This makes programs safer and easier to understand.

Second, it communicates the programmer's intention. When another programmer sees:

const float PI = 3.14159;

it is immediately clear that the value is intended to remain unchanged.

Third, const can allow the compiler to perform certain optimizations because it knows that the object should not be modified through that access path.

3. const with Pointers

Pointers make const slightly more complicated because there are different ways to combine them.

Consider:

const int *ptr;

This means that ptr is a pointer to a constant integer. You can change where ptr points, but you cannot use ptr to modify the integer it points to.

Example:

int a = 10;
int b = 20;

const int *ptr = &a;

ptr = &b;       // Valid
*ptr = 30;      // Error

The pointer can point to another location, but the value cannot be changed through ptr.

Another form is:

int *const ptr = &a;

Here, the pointer itself is constant. It must continue pointing to the same memory location, but the value stored there can be modified.

*ptr = 30;      // Valid
ptr = &b;       // Error

There is also:

const int *const ptr = &a;

This means both the pointer and the value it points to cannot be modified through ptr.

4. The volatile Qualifier

The volatile keyword tells the compiler that the value of a variable may change unexpectedly, outside the normal flow of the program.

For example:

volatile int status;

The compiler must assume that status can change at any time.

This is important when a variable is affected by something external to the currently executing code, such as:

  • Hardware registers

  • Interrupt service routines

  • Memory-mapped devices

  • Certain shared-memory situations

  • External hardware events

5. Why Is volatile Necessary?

Modern compilers perform optimization to make programs faster.

Suppose you write:

int flag = 0;

while (flag == 0) {
    // Wait
}

The compiler might determine that nothing inside the loop changes flag. Depending on the circumstances and optimization, it may optimize the repeated memory access.

However, suppose flag is changed by an interrupt or hardware device. The value really can change even though the current code does not modify it.

In such a situation, you can declare it as:

volatile int flag = 0;

This tells the compiler that it must not assume the value remains unchanged.

For example:

volatile int flag = 0;

while (flag == 0) {
    // Wait for flag to change
}

The compiler must perform the required accesses to flag rather than treating its value as permanently unchanged.

6. volatile and Hardware Programming

One of the most important applications of volatile is accessing hardware registers.

For example:

volatile unsigned int *status_register;

A hardware device may change the value stored at a particular memory address.

The processor might read:

value = *status_register;

The next read could return a different value because the hardware has changed the register.

Without appropriate volatile qualification, the compiler could potentially optimize repeated accesses in ways that are inappropriate for hardware communication.

This is why embedded C programs frequently use declarations such as:

volatile unsigned int *register_address;

7. volatile and Interrupts

Interrupts are another common use case.

Consider:

volatile int interrupt_flag = 0;

An interrupt service routine might change the flag:

void interrupt_handler(void)
{
    interrupt_flag = 1;
}

Meanwhile, the main program may check it:

while (interrupt_flag == 0)
{
    // Wait for interrupt
}

The volatile qualifier tells the compiler that interrupt_flag can change independently of the instructions in the current code flow.

8. Important Difference Between const and volatile

The two keywords have completely different purposes.

Qualifier Main Purpose
const Prevents modification through a particular access path
volatile Tells the compiler that the value may change unexpectedly

For example:

const int value = 100;

means the program should not modify value through that identifier.

Whereas:

volatile int value;

means the compiler should expect value to potentially change outside the normal code flow.

9. Using const and volatile Together

It is possible to use both qualifiers:

const volatile int status;

This may initially seem contradictory, but it has a useful meaning.

It indicates that:

  1. The program should not modify the value through this access path.

  2. The value may nevertheless change because of an external source.

A common example is a read-only hardware register.

The hardware can change the register, but the software should only read it.

For example:

const volatile unsigned int *status_register;

The const part prevents software from modifying the register through the pointer, while volatile tells the compiler that the hardware may change the register at any time.

10. volatile Does Not Make Code Thread-Safe

An important point is that volatile should not be confused with synchronization.

For example:

volatile int counter;

does not automatically make operations such as:

counter++;

atomic.

The operation may involve multiple underlying steps:

Read counter
Add 1
Write counter

If multiple threads access the same variable simultaneously, a race condition can still occur.

For multithreaded programs, synchronization mechanisms such as mutexes, atomic operations, or other concurrency primitives may be required.

Therefore:

volatile ≠ atomic
volatile ≠ thread-safe
volatile ≠ synchronization

11. const in Function Parameters

const is also widely used in functions.

Consider:

void display(const char *message)
{
    printf("%s", message);
}

The function can read the string but should not modify it through message.

This is useful because it clearly communicates that the function does not intend to modify the supplied data.

Another example is:

void calculate(const int numbers[], int size)
{
    // Process numbers
}

Here, const indicates that the function should not modify the elements of the array.

12. Example Combining Both Qualifiers

Consider an embedded system that has a hardware status register:

const volatile unsigned int status_register = 0;

The intended idea is:

const
  |
  +-- Software should not modify the value

volatile
  |
  +-- Hardware may change the value unexpectedly

The program can read the register:

unsigned int status = status_register;

But it should not attempt:

status_register = 10;

because the object is declared const.

13. Practical Example

Suppose a temperature sensor continuously updates a hardware register.

A program might access the register like this:

#define TEMP_REGISTER (*(const volatile unsigned int *)0x40001000)

int main()
{
    unsigned int temperature;

    temperature = TEMP_REGISTER;

    return 0;
}

Here:

const

indicates that the software should not write to the register.

volatile

indicates that the value can change independently because the hardware controls it.

The address 0x40001000 is only an illustrative example; actual hardware addresses depend on the particular microcontroller or device.

14. Key Points to Remember

const is primarily about protecting data from modification through a particular expression or identifier.

volatile is primarily about preventing the compiler from assuming that a value remains unchanged between accesses.

They can be used separately:

const int x = 10;
volatile int y;

or together:

const volatile int z;

The most common applications of const include constants, read-only function parameters, and protecting data accessed through pointers.

The most common applications of volatile include hardware registers, interrupt-modified variables, and other situations where values can change outside the compiler's normal view of program execution.

Most importantly, volatile does not guarantee atomicity or thread safety, and const does not necessarily mean that the underlying memory can never be changed by any mechanism. They describe how the program is allowed or required to access an object.