2. Volatile
The volatile qualifier declares variables that may be modified by external factors, such as hardware devices or other threads, even if the code appears to not modify them directly. It tells the compiler that the value of the variable can change unexpectedly, so it should not make any assumptions or optimizations based on the variable's value.
Example
#include <stdio.h>
volatile int sensor_value;
void read_sensor() {
sensor_value = get_sensor_reading(); // Reading from a hardware sensor
}
int main() {
while (1) {
read_sensor();
printf("Sensor value: %d\n", sensor_value);
// Other code...
}
return 0;
}

You can also try this code with Online C Compiler
Run Code
In this example, `sensor_value` is declared as a volatile variable. The `read_sensor` function reads the value from a hardware sensor and assigns it to `sensor_value`. The `main` function continuously reads the sensor value and prints it.
By declaring `sensor_value` as volatile, the compiler knows that its value can change unexpectedly due to external factors (in this case, the hardware sensor). The compiler will not make any optimizations that assume the value of `sensor_value` remains unchanged between consecutive accesses.
The volatile qualifier is useful in scenarios like:
1. Hardware registers or memory-mapped I/O
2. Shared memory between multiple threads
3. Signal handlers or interrupt service routines
Note: Volatile ensures that the compiler generates code that always reads the variable's value from memory and does not make any assumptions based on previous reads or writes.
3. restrict
The restrict qualifier is used to optimize pointer-related code by allowing the compiler to make certain assumptions about pointer aliasing. It indicates that a pointer is the only way to access the memory it points to within its scope.
Example
#include <stdio.h>
void multiply_arrays(int* restrict a, const int* restrict b, int size) {
for (int i = 0; i < size; i++) {
a[i] *= b[i];
}
}
int main() {
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {2, 3, 4, 5, 6};
int size = sizeof(arr1) / sizeof(arr1[0]);
multiply_arrays(arr1, arr2, size);
printf("Result: ");
for (int i = 0; i < size; i++) {
printf("%d ", arr1[i]);
}
printf("\n");
return 0;
}

You can also try this code with Online C Compiler
Run Code
In this example, the `multiply_arrays` function takes two restrict-qualified pointers, `a` and `b`, and multiplies the corresponding elements of the arrays. The restrict qualifier tells the compiler that `a` and `b` point to distinct memory regions that do not overlap.
By using restrict, the compiler can optimize the code by assuming that the pointers do not alias, meaning that no other pointers in the same scope refer to the same memory location. This allows the compiler to perform optimizations such as loop unrolling, vectorization, or reordering memory accesses.
The output of the program will be:
Result: 2 6 12 20 30
Note that the restrict qualifier is a hint to the compiler and does not affect the program's behavior. It is the programmer's responsibility to ensure that the pointers do not alias within the specified scope.
Note: With the help of restrict you can write more efficient code and improved performance, especially in scenarios which involve pointer-based computations or memory-intensive operations.
4. _Atomic
The _Atomic qualifier is used to declare atomic types, which are types that support atomic operations. Atomic operations are indivisible and uninterruptible, meaning they are guaranteed to complete without interference from other threads.
Example
#include <stdio.h>
#include <stdatomic.h>
#include <threads.h>
_Atomic int counter = 0;
int increment_counter(void* arg) {
for (int i = 0; i < 1000000; i++) {
atomic_fetch_add(&counter, 1);
}
return 0;
}
int main() {
thrd_t thread1, thread2;
thrd_create(&thread1, increment_counter, NULL);
thrd_create(&thread2, increment_counter, NULL);
thrd_join(thread1, NULL);
thrd_join(thread2, NULL);
printf("Final counter value: %d\n", counter);
return 0;
}

You can also try this code with Online C Compiler
Run Code
In this example, `counter` is declared as an atomic integer using the `_Atomic` qualifier. The `increment_counter` function is executed by two threads concurrently, each incrementing the `counter` variable 1,000,000 times using the `atomic_fetch_add` function.
The `atomic_fetch_add` function performs an atomic operation that increments the `counter` variable and returns its previous value. This ensures that the increment operation is performed atomically, without any race conditions or data inconsistencies between threads.
The main function creates two threads using `thrd_create` and waits for them to complete using `thrd_join`. Finally, it prints the final value of the `counter` variable.
The output of the program will be:
Final counter value: 2000000
Why is it useful? The atomic qualifier guarantees that the operations on the atomic variable are performed atomically, even in the presence of multiple threads accessing the variable concurrently. This helps prevent data races and ensures the integrity of shared data in multi-threaded programs.
5. _Thread_local
The `_Thread_local` qualifier is used to declare thread-local storage (TLS) variables. TLS variables are unique to each thread and stored separately for each thread, allowing multiple threads to have their own independent copies of the variable.
Example
#include <stdio.h>
#include <threads.h>
_Thread_local int thread_id;
int thread_func(void* arg) {
thread_id = *(int*)arg;
printf("Thread %d: Thread ID = %d\n", thread_id, thread_id);
return 0;
}
int main() {
thrd_t threads[5];
int thread_args[5];
for (int i = 0; i < 5; i++) {
thread_args[i] = i + 1;
thrd_create(&threads[i], thread_func, &thread_args[i]);
}
for (int i = 0; i < 5; i++) {
thrd_join(threads[i], NULL);
}
return 0;
}

You can also try this code with Online C Compiler
Run Code
In this example, `thread_id` is declared as a `_Thread_local` variable. The `thread_func` function is executed by multiple threads, each having its own copy of `thread_id`. The thread function receives an argument that is used to initialize the `thread_id` variable.
The main function creates five threads using `thrd_create` and passes a unique argument to each thread. Each thread executes the `thread_func` function, which sets the `thread_id` variable to the received argument and prints the thread ID.
The output of the program will be similar to:
Thread 1: Thread ID = 1
Thread 2: Thread ID = 2
Thread 3: Thread ID = 3
Thread 4: Thread ID = 4
Thread 5: Thread ID = 5
The order of the output may vary since the threads execute concurrently.
Why is it useful: The `_Thread_local` qualifier ensures that each thread has its own instance of the variable, preventing data races and allowing threads to have independent local storage. This is useful when you need to store thread-specific data or maintain a per-thread state.
6. _Noreturn
The `_Noreturn` qualifier is used to declare functions that do not return to the caller. It indicates that the function will either terminate the program or loop indefinitely.
Example
#include <stdio.h>
#include <stdlib.h>
_Noreturn void error_exit(const char* message) {
fprintf(stderr, "Error: %s\n", message);
exit(1);
}
int main() {
int denominator = 0;
if (denominator == 0) {
error_exit("Division by zero");
}
int result = 100 / denominator;
printf("Result: %d\n", result);
return 0;
}

You can also try this code with Online C Compiler
Run Code
In this example, the `error_exit` function is declared with the `_Noreturn` qualifier. It takes an error message as a parameter, prints it to the standard error stream using `fprintf`, and then calls the `exit` function to terminate the program with a non-zero status code.
The main function checks if the `denominator` variable is zero. If it is, it calls the `error_exit` function, which prints an error message and terminates the program. If the `denominator` is non-zero, the program continues and performs the division.
The output of the program will be:
Error: Division by zero
The program terminates immediately after printing the error message.
When it can be used: The `_Noreturn` qualifier helps in documenting and optimizing functions that do not return. It allows the compiler to generate more efficient code by eliminating any unnecessary cleanup or return paths after the function call.
7. _Alignas
The `_Alignas` qualifier is used to specify the alignment requirement for variables or types. Alignment refers to the memory address at which an object is allocated. The `_Alignas` qualifier ensures that the variable or type is aligned on a memory boundary that is a multiple of the specified value.
Example
#include <stdio.h>
#include <stdalign.h>
typedef struct {
char c;
_Alignas(8) int i;
} AlignedStruct;
int main() {
AlignedStruct s;
printf("Alignment of AlignedStruct: %zu\n", alignof(AlignedStruct));
printf("Offset of 'c': %zu\n", offsetof(AlignedStruct, c));
printf("Offset of 'i': %zu\n", offsetof(AlignedStruct, i));
return 0;
}

You can also try this code with Online C Compiler
Run Code
In this example, the `AlignedStruct` is defined with a `char` member `c` and an `int` member `i`. The `_Alignas(8)` qualifier is used to specify that the `i` member should be aligned on an 8-byte boundary.
The `main` function declares a variable `s` of type `AlignedStruct`. It then uses the `alignof` macro to print the alignment of `AlignedStruct` and the `offsetof` macro to print the offsets of the `c` and `i` members within the structure.
The output of the program will be:
Alignment of AlignedStruct: 8
Offset of 'c': 0
Offset of 'i': 8
The output shows that `AlignedStruct` has an alignment of 8 bytes. The `c` member is at offset 0, while the `i` member is at offset 8, ensuring that it is aligned on an 8-byte boundary.
Why is it useful: `_Alignas` allows you to control the alignment of variables or types. Proper alignment can have performance benefits, especially when working with certain hardware or memory-mapped structures. It can also be necessary when interacting with external libraries or APIs that have specific alignment requirements.
Frequently Asked Questions
What happens if I try to modify a const variable?
Attempting to modify a const variable will result in a compile-time error. The compiler enforces the constness of the variable & prevents any modifications to its value.
Can volatile variables be optimized by the compiler?
No, the compiler does not optimize volatile variables. It assumes that the value of a volatile variable can change unexpectedly, so it generates code that always reads the variable's value from memory.
Is the restrict qualifier mandatory for pointer arguments?
No, the restrict qualifier is optional. It is a hint to the compiler that allows it to perform optimizations based on the assumption that the pointers do not alias within the specified scope.
Conclusion
In this article, we discussed the different types of qualifiers in C, such as const, volatile, restrict, _Atomic, _Thread_local, _Noreturn, and _Alignas. These qualifiers provide additional information about variables, enabling the compiler to optimize code, ensure data integrity, and enforce specific behavior.
You can also check out our other blogs on Code360.