
In this article, we will explore the qualifiers in C programming (const and volatile).
This is a very important topic in C and embedded systems.
In interviews, this is often the first question about the C language.
What is the volatile keyword? Why use volatile? What is the difference between const and volatile?
What are qualifiers? And so on.
If you can answer this question, they will consider you familiar with the C language.
So, before attending the interview, please read this topic carefully. I believe they will ask questions about qualifiers. Let’s get started.
Qualifiers in CIntroduction
These are new in standard C, although the concept of const is borrowed from C++.
Let’s be clear: the concepts of const and volatile are completely independent.
A common misconception is to think that const is in some way the opposite of volatile, and vice versa.
They are unrelated, and you should remember this fact.
Since the const declaration is simpler, we will discuss it first, but only after we understand where these two types of qualifiers might be used. The complete list of relevant keywords is:
char long float volatile short signed double void int unsigned const
In this list, const and volatile are type qualifiers, while the rest are type specifiers. Various combinations of type specifiers are allowed:
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
There are a few points to note.
All declarations related to int are signed by default, so signed is redundant in this context.
If there are any other type specifiers or qualifiers, the int part can be omitted since that is the default.
The keywords const and volatile can be applied to any declaration, including structures, unions, enumeration types, or typedef names.
Applying them to declarations is called qualifying declarations—this is why const and volatile are referred to as type qualifiers rather than type specifiers. Here are some representative examples:
volatile i; volatile int j; const long q; const volatile unsigned long int rt; struct { const long int li; signed char sc; } volatile vs;
Don’t be intimidated. Some of these are intentionally complex. Their meanings will be explained later. Remember, by introducing storage class specifiers, they can also become more complex! In fact, what is truly striking is
extern const volatile unsigned long int rt_clk;
which is likely to appear 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 through assignment, increment, or decrement.
On ANSI-compliant compilers, the code should produce an error message. However, you can initialize a const variable.
const int nochange; /* qualifies as being constant */ nochange = 12; /* not allowed */
Thus, the following code is correct:
const int nochange = 12; /* ok */
This declaration does not change the read-only variable. Once initialized, it cannot be changed. You can use the const keyword to create data arrays that the program cannot change, such as:
const int days1[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
Using const in Pointer and Parameter Declarations
Using the const keyword in simple variable and array declarations is quite straightforward.
Pointers are more complex because you must distinguish between making the pointer itself const and making the value pointed to by the pointer const.
const float * p; /* p points to a constant float value */ or float const * p;
This declaration specifies that p points to a value that must remain constant.
The value of p itself can change.
For example, it can be set to point to another const value.
Placing const after the type name and before <span>*</span> means that the pointer cannot be used to change the value it points to.
In short, <span>const</span> appearing on the left side of <span>*</span> makes the data constant, while <span>const</span> appearing on the right side of <span>*</span> makes the pointer itself constant.
float * const pr; /* pr is a const pointer */
In contrast, this declaration indicates that the value of pointer pr cannot change. It must always point to the same address, but the value pointed to can change.
const float * const ptr;
Finally, this declaration means that ptr must always point to the same location, and the value stored at that location cannot be changed.
A common use of this new keyword is to declare pointers as formal function parameters. For example, suppose you have a function named <span>display()</span> that displays the contents of an array.
To use it, you need to pass the name of the array as an actual parameter, but the name of the array is an address.
This would allow the function to change the data in the calling function. However, the following prototype prevents this from happening:
void display(const int array[], int limit);
In the prototype and function header, the parameter declaration <span>const int array[]</span> is equivalent to <span>const int * array</span>, so this declaration indicates that the data pointed to by array cannot be changed.
The ANSI C library follows this practice.
If a pointer is only used to allow a function to access a value, that pointer is declared as pointing to a const qualified type.
If a pointer is used to change data in the calling function, the const keyword is not used.
For example, the ANSI C declaration of <span>strcat()</span> is as follows:
char *strcat(char *, const char *);
Recall that <span>strcat()</span> appends a copy of the second string to the end of the first string. This modifies the first string but keeps the second string unchanged. The declaration reflects this.
The Difference Between const char *p, char const *p, and char * const p
This is also an important question and one of the most confusing topics in C.
-
<span>const char *p</span>= cannot use the pointer to change the character pointed to, can change the pointer -
<span>char const *p</span>= cannot use the pointer to change the character pointed to, can change the pointer -
<span>char * const p</span>= address cannot change, must be initialized at declaration -
<span>const char * const p</span>= address and value cannot change, must be initialized at declaration
Tips or Hints: Another rule of thumb is to check the position of const:
-
<span>*</span>before => cannot use the pointer to change the character pointed to -
<span>*</span>after => cannot change the address stored by the pointer
Can we use a pointer to change the value of a const variable?
Yes, we can. But this only applies to local constant variables. We cannot modify global constant variables because const global variables are stored in read-only memory. const local variables are 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; }
Output:
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; }
Output: We will not get output. (The program may crash or exhibit undefined behavior)
Volatile (Qualifier in C)
VOLATILE means: – unstable, unpredictable… etc.
So, the basic meaning of volatile is that we cannot predict what will happen next.
The volatile keyword in programming languages means to inform/tell the compiler not to predict/assume/believe/suppose the value of a specific variable declared as volatile.
The volatile keyword forces the compiler not to store a copy of the variable in a register but to fetch it from memory every time.
Code Optimization
Look at Code 1 and Code 2 below. Assume:
| Code 1 (without Volatile) | Code 2 (with Volatile) |
|
|
In Code 1 and Code 2, we are simply checking the should_run_in_loop variable, and if it is non-zero, we increment count. The state of should_run_in_loop may change asynchronously with the program flow.
When you enable optimization in the IDE or compiler, it will generate assembly code similar to the following.
| Disassembly of Code 1 (without Volatile) | Disassembly of Code 2 (with Volatile) |
|
|
In the disassembly of the non-volatile example, the statement <span>LDR r1, [r0]</span> loads the value of should_run_in_loop into register r1 outside the loop marked as <span>.LBB0_1</span>.
Because should_run_in_loop is not declared as volatile, the compiler assumes that its value cannot be modified externally (since it has not been modified within the current scope).
Since the value of should_run_in_loop has already been read into r1, the compiler omits reloading that variable from memory when optimizations are enabled.
Because its value will not change. The result is an infinite loop marked as <span>.LBB0_1</span>.
In the disassembly of the volatile example, the compiler assumes that the value of should_run_in_loop may change externally, so it does not perform optimizations.
Thus, the value of should_run_in_loop is loaded into register r2 inside the loop marked as <span>.LBB1_1</span>.
Therefore, the assembly code generated for the loop is correct because it reads the value from memory each time.
If we change should_run_in_loop in an ISR or another thread or task, it will read the latest value and act accordingly.
I think you should be clear now.
Why/When do we need volatile?
We need to use volatile variables in the following cases:
-
Memory-mapped peripheral registers
-
Global variables modified by interrupt service routines
-
Global variables in multithreaded applications
If we do not use the volatile qualifier, the following issues may arise:
-
The code works fine before optimization is enabled
-
The code works fine when interrupts are disabled
-
Unstable hardware drivers
-
Individual tasks work fine, but crash when another task is enabled
static int var; void test(void) { var = 1; while (var != 10) continue; }
The above code sets the value in var to 1. It then starts polling that value in a loop until var becomes 10.
An optimizing compiler will notice that no other code can change the value stored in ‘var’, so it assumes it will always remain equal to 1.
Then the compiler replaces the function body with an infinite loop, similar to this:
void test_opt(void) { var = 0; while (1) continue; }
Declaration of volatile
Include the keyword volatile before or after the data type in the variable.
volatile int var; or int volatile var;
Pointer to a volatile variable
volatile int * var; or int volatile * var;
The above statements mean ‘var’ is a pointer to a volatile integer.
Volatile pointer to a non-volatile variable
int * volatile var;
Here var is a volatile pointer to a non-volatile variable/object. This type of pointer is rarely used in embedded programming.
Volatile pointer to a volatile variable
int volatile * volatile var;
If we qualify a structure or union with the volatile qualifier, then the entire contents of the structure/union become volatile. We can also apply the volatile qualifier to individual members of a structure/union.
Uses of the volatile qualifier
Peripheral Registers
Most embedded systems consist of a small number of peripheral devices.
The register values of these peripheral devices may change asynchronously.
Assume there is an 8-bit status register at address 0x1234 in any hypothetical device. What we need to do is poll this status register until it becomes non-zero.
The following code snippet is an incorrect implementation for this scenario/requirement:
UINT1 * ptr = (UINT1 *) 0x1234; // Wait for register to become non-zero. while (*ptr == 0); // Do something else.
Now, there is no code nearby trying to change the value of the register stored in the pointer ‘ptr’ at address (0x1234). A typical optimizing compiler (if optimizations are enabled) would optimize the above code as follows:
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
The compiler’s assumptions when optimizing the code are easy to explain.
It simply takes the value at address location 0x1234 (stored in ‘ptr’) into the accumulator and never updates this value, as clearly, the value at address 0x1234 has never been changed by any nearby code.
Thus, as shown in the code, the compiler replaces it with an infinite loop (comparing the initial zero value stored at address 0x1234 with the constant ‘zero’).
Since the initial value stored at this address is zero and never updated, this loop will run forever. Any code after this point will never execute, and the system will hang.
So, what we essentially need to do is force the compiler to update the value stored at address 0x1234 every time it performs a comparison operation. The volatile qualifier solves this problem for us. See the following code snippet:
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, in each iteration of the loop, the actual value stored at address 0x1234 (stored in ‘ptr’) will be fetched from peripheral memory and checked whether it is zero or non-zero; once the code finds that the value is non-zero, the loop will break.
This is exactly what we want.
For registers with special properties, more subtle issues often arise. For example, many peripherals contain registers that are cleared by reading them. Reading them more (or less) times than expected can lead to very unexpected results.
ISR (Interrupt Service Routine)
Sometimes we check a global variable in the main code, which is only modified by an interrupt service routine.
Assume the serial port interrupt checks each received character to see if it is the ETX character (which presumably indicates the end of a message).
If the character is ETX, the serial port ISR sets a specific variable, such as ‘etx_rcvd’.
Then, elsewhere in the main code, in a loop, we check this ‘etx_rcvd’ until it becomes TRUE, at which point the code exits the loop. Now, look at the following code snippet:
int etx_rcvd = FALSE; void main() { … while (!etx_rcvd) { // Wait } … } interrupt void rx_isr(void) { … if (ETX == rx_char) { etx_rcvd = TRUE; } … }
This code may work with optimizations turned off.
But almost all optimizing compilers will optimize this code to unexpected results.
Because the compiler has no hint that etx_rcvd may be changed somewhere outside the code (as we see in the serial port ISR).
So the compiler assumes that the expression !etx_rcvd will always be true and replaces the code with an infinite loop. Thus, the system will never exit the while loop.
All code after the while loop may even be deleted by the optimizer, or the program may never reach it.
Some compilers may issue warnings, while others may not, depending entirely on the specific compiler.
The solution is to declare the variable etx_rcvd as volatile.
Then all your problems (well, at least some of them) will disappear.
Multithreaded Applications
In multithreaded applications, tasks/threads often communicate through shared memory locations (i.e., through global variables).
Well, the compiler knows nothing about preemptive scheduling, context switching, or any such thing.
So this is similar to the issues we discussed when the interrupt service routine changes peripheral memory registers.
Embedded system programmers must be aware that all shared global variables in a multithreaded environment should be declared as volatile. For example:
int cntr; void task1(void) { cntr = 0; while (cntr == 0) { sleep(1); } … } void task2(void) { … cntr++; sleep(10); … }
Once the compiler’s optimizer is enabled, this code is likely to fail.
Declaring ‘cntr’ as volatile is the correct way to solve the problem.
Some compilers allow you to implicitly declare all variables as volatile.
Resist this temptation, as it is essentially a substitute for thinking.
It may also lead to less efficient code.
Can a variable be both constant (const) and volatile? You can have a constant pointer to a volatile variable, but you cannot have a variable that is both constant and volatile.
Consider the following two code blocks, where the second block is the same as the first but adds the volatile keyword. The gray text between the lines of C code represents the i386/AMD64 assembly code generated by this code during compilation.
{ BOOL flag = TRUE; while( flag ); repeat: jmp repeat; } { volatile BOOL flag = TRUE; mov dword ptr [flag], 1 while( flag ); repeat: mov eax, dword ptr [flag] test eax, eax jne repeat }
In the first code block, the variable ‘flag’ may be cached by the compiler into a CPU register because it does not have the volatile qualifier.
Since no one will change the value in the register, the program will hang in an infinite loop (yes, all code below this block is unreachable code, and compilers like Microsoft Visual C++ know this).
Moreover, this loop is optimized in the equivalent program to the same infinite loop, but without involving variable initialization and fetching.<span>jmp</span> label is equivalent to goto label in C.
The second code block has the volatile qualifier and produces more complex assembly output (initializing ‘flag’ with <span>mov</span> instruction, fetching this flag into CPU register ‘eax’ in the loop, and using <span>test</span> instruction to compare the fetched value with zero, returning to the beginning of the loop if ‘flag’ is not equal to zero. ‘jne’ means ‘jump if not equal’).
All of this is because the volatile keyword prevents the compiler from caching the variable value into a CPU register and instead fetches it in every iteration of the loop.
Such code is not always in an infinite loop because another thread in the same program may change the value of the variable ‘flag’, causing the first thread to exit the loop.
It is important to understand that the volatile keyword is just an instruction to the compiler; it only works at compile time. For example, the fact of using interlocked operations is different from merely being a compiler option, as it generates special assembly instructions.
Thus, interlocked instructions are likely hardware instructions that work at runtime.
Can variables be both Volatile and Const?
This is also an important interview question.
-
Const means the program cannot modify that value
-
Volatile means that value may be modified arbitrarily from outside the program.
These two are independent and not mutually exclusive.
Using them together, for example, in the case of reading a hardware status register.
Const prevents the value from being accidentally modified before compilation, while volatile tells the compiler that this value may change at any time from outside the program.
So this means that the compiled program cannot modify the value of the variable, but that value can be changed externally, so no optimizations will be performed on the variable.
This,
const volatile
will satisfy both requirements and prevent the optimizing compiler from performing optimizations that would incorrectly execute if only using “const”.
const volatile int temp_var;
The above line of code does not mean that temp_var is a volatile integer that will never change. It means it is a volatile integer that you are not allowed to write to.
I can tell you another example.
Suppose we have two applications sharing memory.
Application 1 continuously writes to a specific memory, while another application (Application 2) reads that memory.
We are developing Application 2, which should not write to that memory, only read.
In this case, we can use both const and volatile together.
When we use them together, const prevents Application 2 from modifying, while volatile tells the compiler not to optimize because Application 1 is constantly changing the value here.
Conclusion
-
A volatile variable can be changed by background routines. This background routine may be an interrupt signal from the microprocessor, a thread, a real-time clock, etc.
-
In short, we can say that a volatile variable is a value stored in memory that can be modified by any external source.
-
Whenever the compiler encounters any reference to a volatile variable, it always loads the value of that variable from memory, so if any external source modifies the value in memory, the compiler will get its updated value.
-
Volatile variables work opposite to register variables in C. Therefore, volatile variables require more execution time than non-volatile variables.