Table of contents
1.
Introduction
2.
1. Const
2.1.
Example 1
2.2.
Example 2
2.3.
Advantages of Const qualifier: 
3.
2. Volatile
3.1.
Example
4.
3. restrict
4.1.
Example
5.
4. _Atomic
5.1.
Example
6.
5. _Thread_local
6.1.
Example
7.
6. _Noreturn
7.1.
Example
8.
7. _Alignas
8.1.
Example
9.
Frequently Asked Questions
9.1.
What happens if I try to modify a const variable?
9.2.
Can volatile variables be optimized by the compiler?
9.3.
Is the restrict qualifier mandatory for pointer arguments?
10.
Conclusion
Last Updated: Dec 10, 2024
Easy

Type Qualifiers in C

Author Sinki Kumari
0 upvote
Career growth poll
Do you think IIT Guwahati certified course can help you in your career?

Introduction

Type qualifiers in C are keywords that modify the properties of variables and data types. They allow you to specify how a variable should be treated by the compiler in terms of optimization, memory allocation, and accessibility. Type qualifiers provide additional information about a variable, like whether its value can be modified, how it is stored in memory, or if it can be accessed by multiple threads. Type qualifiers are important for writing efficient & bug-free C code. 

Type Qualifiers in C

 

In this article, we'll discuss the different type qualifiers in C, which are const, volatile, restrict, _Atomic, _Thread_local, _Noreturn & _Alignas, with their examples.

1. Const

The const qualifier is used to declare variables whose values cannot be modified once they are initialized. When a variable is declared with the const qualifier, the compiler ensures that its value remains constant throughout the program's execution. Any attempt to modify a const variable will result in a compile-time error, helping prevent accidental changes & improving code reliability.

Example 1

#include <stdio.h>


int main() {
    const int MAX_SIZE = 100;
    
    printf("Max size: %d\n", MAX_SIZE);
    
    MAX_SIZE = 200;  // Compile-time error: cannot modify a const variable
    
    return 0;
}
You can also try this code with Online C Compiler
Run Code


In this example, `MAX_SIZE` is declared as a const variable with an initial value of 100. The `printf` statement prints the value of `MAX_SIZE`. However, the attempt to modify `MAX_SIZE` by assigning a new value of 200 will result in a compile-time error because const variables cannot be modified.

Example 2

#include <stdio.h>
void print_message(const char* message) {
    printf("%s\n", message);
    
    message[0] = 'h';  // Compile-time error: cannot modify a const char*
}
int main() {
    const char* MESSAGE = "Hello, World!";
    
    print_message(MESSAGE);
    
    return 0;
}
You can also try this code with Online C Compiler
Run Code


In this example, the `print_message` function takes a const char pointer `message` as a parameter. Inside the function, the `printf` statement prints the message. However, attempting to modify the first character of the message by assigning 'h' to `message[0]` will result in a compile-time error because the pointer is declared as const.

Advantages of Const qualifier: 

1. It improves code readability by clearly indicating which variables are not intended to be modified.
 

2. It helps catch unintentional modifications at compile-time, reducing bugs & ensuring program correctness.
 

3. It allows the compiler to perform optimizations, such as placing const variables in read-only memory.

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.

Live masterclass