Storage Classes in C

In this lesson we will explore storage classes in C language, a fundamental concept to understand how variables are managed in memory.

Storage classes allow you to define the lifetime, the visibility or scope, and the linkage of variables, influencing their behavior during program execution.

We will delve into the four main storage classes: auto, static, extern, and register, focusing on the case of variables. Later, we will see how these classes also apply to functions. The last storage class, thread_local, will be covered in a subsequent lesson.

Properties of Variables

Before delving into the study of storage classes, it is necessary to make a premise about the properties of variables in C. Every variable in C has three fundamental properties:

  • Lifetime:

    Also called Storage Duration, it determines when the memory necessary for the variable is allocated and when it is released. Variables can have automatic, static, dynamic, or thread duration.

    When a variable has automatic lifetime, it means that the variable is allocated when the program enters the code block in which it is declared and released when the program exits the code block. All local variables always have automatic duration. This is because, as we have seen, they are allocated on the function stack.

    When, instead, a variable has static lifetime, the variable is allocated when the program begins its execution and released when the program terminates. Global variables, that is, variables declared with the static modifier, have static duration.

    A variable can also be dynamically allocated on the heap of a process. In that case, the lifetime of the variable is dynamic and it is the programmer's responsibility both when to create the variable and when to release it. In that case, we say that the variable has a dynamic lifetime.

    In the C11 standard, since support for threads was introduced, it is possible to declare variables with thread lifetime. These variables are allocated when the thread is created and released when the thread terminates. We will study this topic in upcoming lessons.

  • Visibility or Scope:

    The scope or visibility of a variable is that portion of the program in which the variable is accessible or otherwise referenceable.

    In C language, a variable can be visible at the code block level or it can be visible at the file level and therefore a global variable.

  • Linkage:

    The linkage of a variable determines how the variable is accessible from other source files or portions of the program.

    A variable can be declared with:

    • external linkage: the variable is accessible from other source files of the same program;
    • internal linkage: the variable is accessible only within the source file in which it is declared;

      In that case, the variable is visible only within the source file and can be used in all functions of that file. Another source file can have a variable with internal linkage and with the same name. In this case, the second variable will be treated as a completely different variable.

    • No Linkage: the variable belongs to a single function or code block and is not accessible from other parts of the program.

In general, the three properties seen above depend on the point where a variable is declared. By default, the following rules apply:

  • If a variable is declared inside a code block, including the body of a function, it will have the following properties:

    • Automatic lifetime;
    • Visibility limited to the code block in which it is declared;
    • No linkage.
  • If a variable is declared, instead, outside any code block, that is, at the outermost level of the program, it will have the following properties:

    • Static lifetime;
    • File-level visibility;
    • External linkage.

Let's take the following example:

int x;

void f(void) {
    int y;
}

In this example, we have the variable y which has automatic duration, visibility limited to function f, and no linkage. The variable x, instead, has static duration, file-level visibility, and external linkage.

In most cases, this default behavior is fine. However, in some cases, it may be necessary to manually specify the properties of a variable. To do this, C provides storage classes. Let's examine them in detail.

Storage Class auto

The storage class auto is legal only for variables that are declared inside a code block.

Definition

Storage Class auto

In C language, a variable declared with storage class auto has the following properties:

  • Automatic lifetime: the variable is allocated when the program enters the code block in which it is declared and released when the program exits the code block;
  • Visibility limited to the code block in which it is declared;
  • No linkage.

Since local variables in C already have these properties by default, the use of the storage class auto is superfluous, in fact it is always omitted. However, it is possible to use it to make the default behavior explicit.

void f(void) {
    // In this case auto is superfluous
    auto int x;
}

Since the behavior of auto is the default for local variables, local variables are often also called Automatic Variables.

Storage Class static

The storage class static is legal for both local variables and global variables. However, its effect changes depending on where the variable is declared.

  • If the variable is declared outside any code block, therefore at the file level, the static class specifies that the variable has Internal Linkage. In other words, the variable is visible only at the level of the file in which it is declared.
  • If the variable is declared inside a code block, the static class specifies that the variable has Static Lifetime. In other words, the variable is allocated when the program begins its execution and released when the program terminates.

Let's see an example:

static int x;

void f(void) {
    static int y;
}

When we declare a variable outside any block as static (in the example the variable x), we are essentially hiding the variable from other source files. Only functions that appear in the same file, as in the case of function f, can access it.

For example:

static int x;

void f1(void) {
    /* f1 can access x */
}

void f2(void) {
    /* f2 can access x */
}

When a variable is declared inside a code block as static, the variable has static duration, that is, it will always reside in the same memory location for the entire duration of the program. Therefore, unlike automatic variables, which lose their value every time the program exits the block in which they are contained, a static variable will maintain its value indefinitely.

static variables have some important properties:

  • A static variable defined inside a block is initialized only once, before the start of program execution. Conversely, an automatic variable is initialized every time the code block in which it is contained is executed, provided, of course, that it has an initializer.
  • Every time a function invokes itself in a recursive manner, a new set of local or automatic variables is created. If the function, however, has a static variable, this will be shared among all recursive calls.
  • We have seen that functions cannot return a pointer to a local or automatic variable. However, a function can return a pointer to a static variable.

The power of static variables lies in the fact that they can be used to maintain the state of a function between different calls. In other words, a function can save data in a hidden area of memory and retrieve it on the next call. This area is not accessible from other parts of the program.

static variables are often used to make programs more efficient.

Let's take an example. Suppose we want to create a function that takes as input a value between 0 and 15 and returns its corresponding hexadecimal value. A possible implementation is the following:

char hexadecimal_digit(int n) {
    const char digits[] = "0123456789ABCDEF";
    return digits[n];
}

In this function, the digit passed as an argument is transformed into the hexadecimal equivalent by exploiting an array of characters. However, the array digits is created at every call of the function. This involves a waste of resources, since the array is always the same and does not change. We can improve the implementation by using a static variable:

char hexadecimal_digit(int n) {
    static const char digits[] = "0123456789ABCDEF";
    return digits[n];
}

Since static variables are initialized only once, the array digits will be created only once and will be shared among all calls of the function.

In summary:

Definition

Storage Class static

In C language, a variable declared with storage class static has the following properties:

  • Static lifetime: the variable is allocated when the program begins its execution and released when the program terminates;
  • Visibility limited to the code block in which it is declared if the variable is declared inside a code block, or file-level visibility if the variable is declared outside a code block;
  • Internal linkage if the variable is declared outside a code block, or no linkage if the variable is declared inside a code block.

Storage Class extern

The storage class extern allows multiple source files to share the same variable.

We have already addressed the use of extern when we saw how to use header files to share global variables.

In general, it should be remembered that when we declare a variable in this way:

extern int x;

we are telling the compiler that the variable x is of integer type but that its memory allocation occurs in another source file. Technically, this is a declaration but not a definition. The actual definition of the variable x occurs in another source file.

A variable, in fact, can have multiple declarations but only one definition. The definition is what actually reserves memory for the variable. Declarations, instead, inform the compiler of the existence of the variable.

However, there is an exception to this rule. If a variable is declared as extern and at the same time initialized, then the declaration also becomes a definition. In that case, the variable x is allocated in the same source file in which it is declared.

extern int x = 10;

The line above is to all intents and purposes identical to the following line:

int x = 10;

This rule was introduced in order to prevent multiple extern declarations from being able to initialize a variable at the same time.

A variable declared as extern always has static lifetime.

The scope or visibility of an extern variable depends on where it is declared:

  • If it is declared inside a code block, the variable is visible only inside the code block itself;
  • If it is declared outside a code block, the variable is visible at the file level.
/* Static lifetime */
/* File-level visibility */
extern int a;

void f(void) {
    /* Static lifetime */
    /* Visibility limited to code block */
    extern int b;
}

As for linkage, the rules are slightly more complex.

  • If the variable was previously declared in the file as static outside any function, then it will have internal linkage;
  • Otherwise, the variable will have external linkage. This is the default behavior.

In summary:

Definition

Storage Class extern

In C language, a variable declared with storage class extern has the following properties:

  • Static lifetime;
  • Visibility limited to the code block in which it is declared if the variable is declared inside a code block, or file-level visibility if the variable is declared outside a code block;
  • Internal linkage if the variable is declared outside a code block and previously declared as static, or external linkage if the variable is declared outside a code block and has not been declared as static.

Storage Class register

If we declare a variable with the storage class register, we are telling the compiler that the variable will be used very frequently and that, therefore, it is convenient to allocate it in a CPU register.

register int x;

In fact, a register is a small memory area of the processor that allows storing temporary data and values. Since access to RAM is a very slow operation compared to access to registers, the use of register variables can make the program faster.

However, it should be kept in mind that the use of the register class is only a request to the compiler and not a command. In other words, the compiler, for optimization reasons or otherwise, may ignore the request to allocate the variable in a register and decide to allocate it in memory.

The storage class register can be used only for local variables defined inside a code block. It is not possible to declare global variables as register. For this reason, register variables have the same properties as auto variables:

  • Automatic lifetime;
  • Visibility limited to the code block in which they are declared;
  • No linkage.

However, register variables have a fundamental difference compared to other variables: it is not possible to obtain the address of a register variable. This is because, as we said, register variables are allocated in CPU registers and not in memory. Therefore, an operation of this kind is illegal:

register int x;

// ERROR: it is not possible to obtain
// the address of a register variable
int *p = &x;

This restriction also applies in the case where the compiler has chosen to store the variable in memory.

The register class is very useful in cases where a variable is accessed or modified very frequently. For example, a typical use of register variables is inside for loops. If we want to write a function that calculates the average of an array of double, we can write:

double average(const double *array, size_t n) {
    register double sum = 0.0;
    register size_t i;

    for (i = 0; i < n; i++) {
        sum += array[i];
    }

    return sum / n;
}

In this case, both the variable sum and the variable i are allocated in CPU registers. This makes the program faster compared to the case where the variables were allocated in memory.

In summary:

Definition

Storage Class register

In C language, a variable declared with storage class register has the following properties:

  • Automatic lifetime;
  • Visibility limited to the code block in which it is declared;
  • No linkage.

You can declare only local variables as register.

Furthermore, it is not possible to obtain the address of a register variable.

Nowadays, the use of the storage class register is very limited. Modern compilers are able to optimize code much more effectively than in the past. For this reason, it is often better to leave to the compiler the choice of allocating variables in registers or in memory.

However, using the register class still provides useful information that the compiler can exploit to optimize the code. Therefore, if you are sure that a variable will be used very frequently, it is possible to use the register class to give a hint to the compiler. Furthermore, with the register class you can prevent the address of a variable from being taken and therefore the value from being modified through a pointer. In this sense, register is very similar to the qualifier restrict applied to pointers that we have already studied previously.

Summary

In this lesson we have seen that variables in C language have three fundamental properties:

  • Lifetime;
  • Visibility or Scope;
  • Linkage.

To modify these properties, C provides four main storage classes:

  • auto: for local variables with automatic duration;
  • static: for variables with static duration and limited visibility;
  • extern: for variables with static duration and global visibility;
  • register: for local variables allocated in CPU registers.

Each of these storage classes has specific properties that allow customizing the behavior of a variable.

There is an additional storage class introduced starting with the C11 standard: thread_local. This class allows declaring variables with thread lifetime. To study it in detail, however, we must study threads in C11, which we will do in upcoming lessons.

In the next lesson, instead, we will study storage classes applied to functions.