Type Qualifiers in C

Type Qualifiers in C language are used to specify how a value can change over time.

In C Language there are four of them:

  • const
  • volatile
  • restrict
  • _Atomic

We have already studied the restrict qualifier when we covered restricted pointers and, in fact, it only applies to pointers.

The volatile qualifier is used for low-level programming and we will study it in upcoming lessons. Meanwhile, the _Atomic qualifier is used for concurrent programming and its study is also postponed to later lessons.

In this lesson we will focus on the const qualifier and its usage.

const Qualifier

The const qualifier placed before the declaration of a variable allows defining entities that resemble a variable but which can actually only be accessed in read-only mode. In other words, a program can access the value of a const object but cannot modify its content.

Let's take the following example:

const int number = 10;

In this case we have created a const object of type int called number and assigned it the value 10. This means that the value of number cannot be modified during program execution.

Similarly, we can create an array of const:

const int numbers[] = {1, 2, 3, 4, 5};

In this case we have created a const array of type int called numbers and assigned it the values {1, 2, 3, 4, 5}. Also in this case, the content of the numbers array cannot be modified.

Declaring an object as const provides a number of advantages:

  • Code readability:

    the const qualifier indicates that the value of an object will not change during the program, making the code more readable and easier to maintain. Furthermore, a developer reading the code can immediately understand that an object is intended to remain unchanged.

  • Error prevention:

    the const qualifier prevents accidental modification of an object. If a program attempts to modify a const object, the compiler will generate an error.

  • Code optimization:

    the const qualifier allows the compiler to perform code optimizations. For example, the compiler can store a const object in a different memory location than a non-const object, since it knows that the value of a const object will not change.

  • Embedded programming:

    the const qualifier is particularly useful in embedded programming, where memory is limited and error prevention is fundamental. Furthermore, the const qualifier can be used to store constants in read-only memory (ROM). For example, when developing for platforms like Arduino, it is common to use the const qualifier to define constants that are stored in EEPROM or Flash memory rather than in RAM.

Recapping:

Definition

const Qualifier in C Language

The const qualifier in C language allows defining entities that can be accessed in read-only mode. const variables cannot be modified during program execution.

The syntax for using the const qualifier is as follows:

const type name = value;

where type is the data type of the object, name is the name of the object and value is the initial value of the object.

Note

A const object must always be initialized

A const object must always be initialized at the time of declaration:

const int number = 10; // Correct

Actually, it is possible to declare a const object without initializing it. However, this practice not only makes little sense, but the consequence is that the object cannot be modified. Therefore, the value it will contain will be completely random.

For example, if we write a program like this:

#include <stdio.h>

int main() {
    /* Not Initialized */
    const int number;
    printf("Number: %d\n", number);
    return 0;
}

The program might print a random value, such as 0, 1, -1, etc. This is because the number object has not been initialized and its value is undefined.

Difference between const and #define

We have previously seen that in C code we can define constants through the #define preprocessing directive.

At this point the question naturally arises: why provide two different mechanisms for defining constants?

The fact is that there are substantial differences between using the const qualifier and the #define directive:

  • The #define directive can be used to create constants of numeric, character or string type. Meanwhile, the const qualifier can be used to create constants of any type: arrays, data structures, pointers and unions.
  • Constants defined with #define are replaced by the preprocessor with the defined value. Meanwhile, constants defined with const are stored in memory and can be treated as normal variables.

    The consequence is that const objects are subject to the same visibility rules to which variables are subject. For example, a const object declared inside a function is only visible inside that function. Meanwhile, a constant defined with #define is visible throughout the source file.

  • The value of a const object can be seen by the debugger, while the value of a constant defined with #define cannot.

  • Constants defined with #define are not typed. Meanwhile, constants defined with const are typed and the compiler can perform type checking.
  • The address operator & can be applied to get the address of a const object since it actually occupies space in memory. Meanwhile, the address operator cannot be applied to a constant defined with #define since it does not occupy memory space.

Beyond these differences, there is a fundamental one that concerns the use of const objects in constant expressions.

In the C89 standard, a const object cannot be used in a constant expression. Therefore, for example, the following code is not valid:

#define N 10

const int M = 10;

/* Valid */
int a[N];

/* NOT Valid in C89 */
int b[M];

In this example, N is a constant defined with #define and M is a const object. In C89, N can be used to define the size of an array, while M cannot.

In the C99 standard this limitation has been relaxed. Therefore a const object can be used in a constant expression provided that the const object has auto as its storage class, that is, has automatic lifetime.

Therefore, in C99, the following code becomes valid:

static const int N = 10;

void function() {
    const int M = 10;

    /* Valid in C99 */
    int a[M];

    /* NOT Valid in C99 */
    int b[N];
}

In this example, N is a const object with static storage class, while M is a const object with auto storage class. In C99, M can be used to define the size of an array, while N cannot.

In general, there is no definitive rule to follow for choosing whether to use const or #define to define constants. The choice depends on the context and the specific needs of the program.

A rule of thumb is to use constants defined with the #define directive when dealing with numeric constants or simple strings. This is because, by doing so, we can use constants in defining array size, in switch statements and in all cases where constant expressions are required.

In Summary

In this lesson we have seen how to use the const qualifier in C language to define constants. Constants defined with const are objects that can be accessed in read-only mode and cannot be modified during program execution.

We have also compared the use of the const qualifier with the #define directive for defining constants and we have seen the differences between the two approaches.

The const qualifier is a powerful tool that can improve code readability, prevent errors and optimize code. It is particularly useful in embedded programming and in situations where it is necessary to define constants of any type.

Furthermore, we have seen that in C99 it is possible to use a const object in a constant expression provided that the const object has auto as its storage class.

In conclusion, the const qualifier is a fundamental tool for writing robust and maintainable C code.