Qualifier in C (Const and Volatile)

This article is the continuation of the Series on the C programming tutorial and carries the discussion on C language programming and its implementation. It aims to provide easy and practical examples for understanding the C program. In our last article, we have seen the Typecasting in C programming. In this article, we are going to see Qualifier in C programming (Const and Volatile).

This is a very important topic in the C and embedded domain. In an interview, this would be the first question in the C language. What is the volatile keyword? Why do we use Volatile? What is the difference between const and volatile? What is a qualifier? etc. If you answered this question then they think you are familiar with the C language. So, before going to the interview please go through this topic. I’m sure they will ask questions from the qualifier. Let’s start.

You can also read pointers 1, pointers 2, different types of pointers, embedded interview topics, and macro vs inline.

Qualifier in C


These are new in Standard C, although the idea of const has been borrowed from C++. Let us get one thing straight: the concepts of const and volatile are completely independent. A common misconception is to imagine that somehow const is the opposite of volatile and vice versa. They are unrelated and you should remember the fact. Since const declarations are simpler, we’ll look at them first, but only after we have seen where both of these type qualifiers may be used. The complete list of relevant keywords is

  • char
  • long
  • float
  • volatile
  • short
  • signed
  • double
  • void
  • int
  • unsigned
  • const

In that list, const and volatile are type qualifiers, the rest are type specifiers. Various combinations of type specifiers are permitted:

  • char signed char, unsigned char int, signed int, unsigned int
  • short int, signed short int, unsigned short int long int, signed long int, unsigned long int float
  • double
  • long double

A few points should be noted. All declarations to do with an int will be signed anyway, so signed is redundant in that context. If any other type specifier or qualifier is present, then the int part may be dropped, as that is the default.

The keywords const and volatile can be applied to any declaration, including those of structures, unions, enumerated types or typedef names. Applying them to a declaration is called qualifying the declaration—that’s why const and volatile are called type qualifiers, rather than type specifiers. Here are a few representative examples:

volatile i; 
volatile int j; 
const long q;
const volatile unsigned long int rt;
    const long int li;
    signed char sc;
}volatile vs;

Don’t be put off. Some of them are deliberately complicated. What they mean will be explained later. Remember that they could also be further complicated by introducing storage class specifications as well! In fact, the truly spectacular

extern const volatile unsigned long int rt_clk;

is a strong possibility in some real-time operating system kernels.

Const or Constant (qualifier in c)

The const keyword in a declaration establishes a variable whose value cannot be modified by assignment or by incrementing or decrementing. On an ANSI-compliant compiler, the code should produce an error message. You can, however, initialize a const variable.

const int nochange; /* qualifies as being constant */
nochange = 12; /* not allowed */

Therefore, the following code is fine:

const int nochange = 12; /* ok */

The preceding declaration makes no change to a read-only variable. After it is initialized, it cannot be changed. You can use the const keyword to, for example, create an array of data that the program can’t alter:

const int days1[12] = {31,28,31,30,31,30,31,31,30,31,30,31};

Using const with Pointers and Parameter Declarations

Using the const keyword when declaring a simple variable and an array is pretty easy. Pointers are more complicated because you have to distinguish between making the pointer itself const and making the value that is pointed to const.

const float * p; /* p points to a constant float value */
float const * p;

The declaration establishes that p points to a value that must remain constant. The value of p itself can be changed. For example, it can be set to point at another const value. Placing const after the type name and before the * means that the pointer can’t be used to change the pointed-to value. In short, a const anywhere to the left of the * makes the data constant, and a const to the right of the * makes the pointer itself constant.

float * const pr; /* pr is a const pointer */

In contrast, the declaration says that the pointer pr itself cannot have its value changed. It must always point to the same address, but the pointed-to value can change.

const float * const ptr;

Finally, the declaration means both that ptr must always point to the same location and that the value stored at the location must not change.

One common use for this new keyword is declaring pointers that serve as formal function parameters. For example, suppose you have a function called display() that displays the contents of an array. To use it, you would pass the name of the array as an actual argument, but the name of an array is an address. That would enable the function to alter data in the calling function. But the following prototype prevents this from happening:

void display(const int array[], int limit);

In a prototype and a function header, the parameter declaration const int array[] is the same as const int * array, so the declaration says that the data to which array points cannot be changed.

The ANSI C library follows this practice. If a pointer is used only to give a function access to values, the pointer is declared as a pointer to a const-qualified type. If the pointer is used to alter data in the calling function, the const keyword isn’t used. For example, the ANSI C declaration for strcat() is this:

char *strcat(char *, const char *);

Recall that strcat() adds a copy of the second string to the end of the first string. This modifies the first string but leaves the second string unchanged. The declaration reflects this.

Difference between const char *p, char const *p and char *const p

This is also an important question. And it is a most confusing topic in C.

  • const char * = Can’t change pointed characters using a pointer, can change the pointer
  • char const *p = Can’t change pointed characters using a pointer, can change the pointer
  • char * const p = Address cannot be changed, Must be initialized at declaration time
  • const char * const p = Both address and value cannot be changed, Initialized at declaration time

Trick or Tips:
Another thumb rule is to check where const is:

before * => Can’t change pointed characters using pointer
after *    => Can’t change pointer stored address

Can we change the value of the const variable by using a pointer?

Yes. We can. But it is only applicable for constant local variables. We can’t modify the Constant global variable, because const global variable is stored in read-only memory. Const local variable is stored in stack memory.

Code 1:

int main() 
    const int a = 10; 
    int *b = &a; 
    printf("Value of constant is %d",a); 
    *b = 20; 
    printf("Value of constant is %d",a); 
    return 0; 


Value of constant is 10
Value of constant is 20

Code 2:

const int a=10; 
int main() 
    int *b = &a; 
    printf("Value of constant is %d",a); 
    *b = 20; 
    printf("Value of constant is %d",a); 
    return 0; 


We will not get output.

Volatile (qualifier in c)


So, the basic meaning of volatile is we can’t predict what is going to happen next. The significance of a volatile keyword in a programming language is to inform/tell the compiler not to pre-predict/assume/believe/presume the value of the particular variable which has been declared as volatile.

The volatile keyword forces the compiler to not store a copy of the variable in the registers and fetch it each time from memory.

Code optimization

Look at code 1 and code 2, below. Suppose:-

Code 1 (Without Volatile) Code 2 (With Volatile)
int should_run_in_loop;
int func(void)
    int count = 0;
    while (should_run_in_loop)
    return count;
volatile int should_run_in_loop;
int func(void)
    int count = 0;
    while (should_run_in_loop)
    return count;

In code 1 and code 2, we are just checking the should_run_in_loop variable and if that is a non-zero value, then we are incrementing the count. The state of should_run_in_loop can changes asynchronously with program flow.

When you enable the optimization in the IDE or compiler, it will produce an assembly code like below.

Disassembly code of Code 1 (Without Volatile) Disassembly code of Code 2 (With Volatile)
        movw    r0, :lower16:should_run_in_loop
        movt    r0, :upper16:should_run_in_loop
        ldr     r1, [r0]
        mvn     r0, #0
        add     r0, r0, #1
        cmp     r1, #0
        beq     .LBB0_1     ; infinite loop
        bx      lr
        movw    r1, :lower16:should_run_in_loop
        mvn     r0, #0
        movt    r1, :upper16:should_run_in_loop
        ldr     r2, [r1]     ; buffer_full
        add     r0, r0, #1
        cmp     r2, #0
        beq     .LBB1_1
        bx      lr

In the disassembly of the nonvolatile example, the statement LDR r1, [r0] loads the value of should_run_in_loop into register r1 outside the loop labeled .LBB0_1. Because should_run_in_loop is not declared as volatile, the compiler assumes that its value cannot be modified outside the program as this is not modified in this current scope. Having already read the value of should_run_in_loop into r0, the compiler omits to reload the variable from the memory when optimizations are enabled. Because its value cannot change. The result is the infinite loop labeled .LBB0_1.

In the disassembly of the volatile example, the compiler assumes that the value of should_run_in_loop can change outside the program and performs no optimization. Therefore, the value of should_run_in_loop is loaded into a register r2 inside the loop labeled .LBB1_1. As a result, the assembly code that is generated for loop .LBB1_1 is correct as it is reading the value from the memory every time. If we change the should_run_in_loop  in the ISR or another thread or another task, it will read the latest value and act accordingly.

I think you are clear.

Why/When do we need volatile?

In the following case, we need to use the volatile variable.

  • Memory-mapped peripheral registers
  • Global variables modified by an interrupt service routine
  • Global variables within a multi-threaded application

If we do not use volatile qualifiers the following problems may arise:

  • Code that works fine until you turn optimization on
  • Code that works fine as long as interrupts are disabled
  • Flaky hardware drivers
  • Tasks that work fine in isolation yet crash when another task is enabled
static int var;
void test(void)
    var = 1;
    while (var != 10)

The above code sets the value in var to 1. It then starts to poll that value in a loop until the value of var becomes 10.

An optimizing compiler will notice that no other code can possibly change the value stored in ‘var’, and therefore assume that it will remain equal to 0 at all times. The compiler will then replace the function body with an infinite loop, similar to this:

void test_opt(void)
    var = 0; 
    while (1) 

Declaration of volatile

Include the keyword volatile before or after the data type in the variable.

volatile int var;
int volatile var;

Pointer to a volatile variable

volatile int * var;
int volatile * var;

The above statements implicate ‘var’ is a pointer to a volatile integer.

Volatile pointers to non-volatile variables

int * volatile var;

Here var is a volatile pointer to a non-volatile variable/object. This type of pointer is very rarely used in embedded programming.

Volatile pointers to volatile variables

int volatile * volatile var;

If we qualify a struct or union with a volatile qualifier, then the entire contents of the struct/union becomes volatile. We can also apply the volatile qualifier to the individual members of the struct/union.

Usages of volatile qualifier

Peripheral registers

Most embedded systems consist of a handful of peripherals devices. The value of the registers of these peripheral devices may change asynchronously. Let’s say there is an 8-bit status register at address 0x1234 in any hypothetical device. What we need to do is to poll this status register until it becomes non-zero. The following code snippet is an incorrect implementation of this scenario/requirement:

UINT1 * ptr = (UINT1 *) 0x1234;
// Wait for register to become non-zero. 
while (*ptr == 0);
// Do something else.

Now no code in proximity attempts to change the value in the register whose address(0x1234) is kept in the ‘ptr’ pointer. A typical optimizing compiler(if optimization is turned ON) will optimize the above code as below:

mov ptr, #0x1234 -> move address 0x1234 to ptr
mov a, @ptr -> move whatever stored at ‘ptr’ to accumulator 
loop bz loop -> go into infinite loop

What the assumes while optimizing the code is easy to interpret. It simply takes the value stored at the address location 0x1234(which is stored in ‘ptr’) into the accumulator and it never updates this value because apparently the value at the address 0x1234 never gets changed(by any nearby code). So, as the code suggests, the compiler replaces it with an infinite loop (comparing the initial zero value stored at the address 0x1234 with a constant ‘zero’). As the value stored at this address would initially be zero and it is never updated, this loop goes forever. The code beyond this point would never get executed and the system would go into a hanged state.

So, what we essentially need to do here is to force the compiler to update the value stored at the address 0x1234 whenever it does the comparison operation. The volatile qualifier does the trick for us. Look at the code snippet below:

UINT1 volatile * ptr = (UINT1 volatile *) 0x1234;

The assembly for the above code should be:

mov ptr, #0x1234 -> move the address 0x1234 to ptr
loop mov a, @ptr -> move whatever stored @address to accumulator 
bz loop -> branch to loop if accumulator is zero

So, now at every loop the actual value stored at the address 0x1234(which is stored in the ‘ptr’) is fetched from the peripheral memory and checked whether it’s zero or non-zero; as soon as the code finds the value to be non-zero the loop breaks. And that’s what we wanted.
Subtler problems tend to arise with registers that have special properties. For instance, a lot of peripherals contain registers that are cleared simply by reading them. Extra (or fewer) reads than you are intending can cause quite unexpected results in these cases.

ISR(Interrupt Service Routine)

Sometimes we check a global variable in the main code and the variable is only changed by the interrupt service routine. Let’s say serial port interrupt checks each received character to see if it is an ETX character (presumably signifying the end of a message). If the character is an ETX, the serial port ISR sets a particular variable, say ‘etx_rcvd’. And from the main code somewhere else this ‘etx_rcvd’ is checked in a loop and until it becomes TRUE the code waits at this loop. Now, let’s check the code snippet below:

int etx_rcvd = FALSE;
void main()
    while (!ext_rcvd) {
        // Wait

interrupt void rx_isr(void)
    if (ETX == rx_char) {
        etx_rcvd = TRUE;

This code may work with optimization turned off. But almost all the optimizing compiler would optimize this code to something which is not intended here. Because the compiler doesn’t even have any hint that etx_rcvd can be changed outside the code somewhere( as we saw within the serial port ISR). So the compiler assumes the expression !ext_rcvd would always be true and would replace the code with an infinite loop. Consequently, the system would never be able to exit the while loop. All the code after the while loop may even be removed by the optimizer or never be reached by the program. Some compiler may throw a warning, or some may not, depends completely on the particular compiler.

The solution is to declare the variable etx_rcvd to be volatile. Then all of your problems (well, some of them anyway) will disappear.

Multi-threaded applications

Often tasks/threads involved in a multi-threaded application communicate via a shared memory location i.e. through a global variable. Well, a compiler does not have any idea about preemptive scheduling or to say, context switching or whatsoever. So this is sort of the same problem as we discussed in the case of an interrupt service routine changing the peripheral memory register. Embedded Systems Programmer has to take care that all shared global variables in a multi-threaded environment be declared volatile. For example:

int cntr;
void task1(void)
     cntr = 0;
     while (cntr == 0) {

void task2(void)

This code will likely fail once the compiler’s optimizer is enabled. Declaring ‘cntr’ to be volatile is the proper way to solve the problem. Some compilers allow you to implicitly declare all variables as volatile. Resist this temptation, since it is essentially a substitute for thought. It also leads to potentially less efficient code.

Can you have a constant volatile variable? You can have a constant pointer to a volatile variable but not a constant volatile variable. Consider the following two blocks of a program, where the second block is the same as the first but with the volatile keyword. Gray text between lines of C code means i386/AMD64 assembler compiled from this code.

    BOOL flag = TRUE; 
    while( flag ); 
    jmp repeat;
     volatile BOOL flag = TRUE; 
     mov dword ptr [flag], 1 
     while( flag );
     mov eax, dword ptr [flag]
     test eax, eax 
     jne repeat

In the first block variable ‘flag’ could be cached by the compiler into a CPU register, because it does not have a volatile qualifier. Because no one will change the value at a register, the program will hang in an infinite loop (yes, all code below this block is unreachable code, and compiler such as Microsoft Visual C++ knows about it). Also, this loop was optimized in the equivalent program with the same infinite loop, but without involving variable initialization and fetching. ‘jmp‘label means the same as ‘goto label’ in C code.

The second block has a volatile qualifier and has a more complex assembler output (initializing ‘flag’ with ‘mov’ Instruction, in a loop fetching this flag into CPU register ‘eax’ with a ‘mov’ instruction, comparing the fetched value with zero with ‘test’ instruction, and returning to the beginning of the loop if ‘flag’ was not equal to zero. ‘jne’ means ’goto if not equal’). This is all because the volatile keyword prohibits the compiler to cache a variable value into a CPU register, and it is fetched in all loop iterations. Such code is not always in an infinite loop because another thread in the same program potentially could change the value of the variable ‘flag’ and the first thread will exit the loop.

It is important to understand that the volatile keyword is just a directive for the compiler and it works only at a compile-time. For example, the fact of using interlocked operation differs from just a compiler option, since special assembler commands are produced. Thus, interlocked instructions are most likely to be hardware directives, and they work at run-time.

Can a variable be both Volatile and Const?

This is also an important interview question. 

  • Const means the program cannot modify the value
  • Volatile means the value may be arbitrarily modified outside the program.

The two are separate and not mutually exclusive. Use them together, for instance, in the case of reading a hardware status register. const prevents the value from being stomped on before compilation, while volatile tells the compiler that this value can be changed at any time external to the program.
So it means that the compiled program cannot modify the variable’s value, but the value can be modified from the outside, thus no optimizations will be performed on the variable.

const volatile <type> <variable name>

Will this satisfy both requirements and prevent an optimizing compiler from incorrectly optimizing the code, that it would do if only “const” were used.

const volatile int temp_var;

The above code line does not mean that temp_var is a volatile integer that never changes. It means it’s a volatile integer that you are not allowed to write to.

I can tell you another example. Let’s assume that we have two application which has shared memory. Application 1 is kept on writing into particular memory and another application (Application 2) is reading that memory. We are developing application 2 which should not write into that memory but only needs to read. In this case, we can use const volatile together. When we use this together const will not allow application 2 to not modify and volatile tells the compiler not to optimize that because application 1 keeps on changing the value here.


  1. A volatile variable can be changed by the background routine of the pre-processor. This background routine may be interrupt signals by a microprocessor, threads, real-time clocks, etc.
  2. In simple words, we can say a value volatile variable that has been stored in the memory can be by any external source.
  3. Whenever the compiler encounters any reference of a volatile variable always loads the value of the variable from memory so that if any external source has modified the value in the memory compiler will get its updated value.
  4. The working principle of the volatile variable is opposite to the register variable in c. Hence volatile variables take more execution time than non-volatile variables.

In our next article, we will see Little Endian and Big Endian in C programming.

You can also read the below tutorials.

Linux Device Driver TutorialsC Programming Tutorials
FreeRTOS TutorialsNuttX RTOS Tutorials
RTX RTOS TutorialsInterrupts Basics
I2C Protocol – Part 1 (Basics)I2C Protocol – Part 2 (Advanced Topics)
STM32 TutorialsLPC2148 (ARM7) Tutorials
PIC16F877A Tutorials8051 Tutorials
Unit Testing in C TutorialsESP32-IDF Tutorials
Raspberry Pi TutorialsEmbedded Interview Topics
Reset Sequence in ARM Cortex-M4BLE Basics
VIC and NVIC in ARMSPI – Serial Peripheral Interface Protocol
STM32F7 Bootloader TutorialsRaspberry PI Pico Tutorials
STM32F103 Bootloader TutorialsRT-Thread RTOS Tutorials
Zephyr RTOS Tutorials - STM32Zephyr RTOS Tutorials - ESP32
AUTOSAR TutorialsUDS Protocol Tutorials
Product ReviewsSTM32 MikroC Bootloader Tutorial
VHDL Tutorials

Notify of

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Inline Feedbacks
View all comments
Table of Contents