Type Qualifiers and Storage Specifiers


static

In addition to specifying the data type when declaring variables, functions or other elements of C/C++, there are type qualifiers and storage specifiers that can be used on declaration.

#include <cstdint>
#include <iostream>
using namespace std;

int main(int argc, char* argv[]) { 
    
    for (int k = 0; k <3; k++){
        uint16_t _a = 3;
        static uint16_t _b = 3; /* Declaring a static variable */
        cout << _a++ << endl; /* Output: 3, 3, 3. Will be reinitialized every loop */
        cout << _b++ << endl; /* Output: 3, 4, 5. Will be initialized only once when declared */
    }
    return 0;
}
Output:
3
3
3
4
3
5

This is a for-loop where the code within this scope is repeatedly executed three times. We will discuss for-loops separately.

for (int k = 0; k <3; k++){
    uint16_t _a = 3;
    static uint16_t _b = 3; /* Declaring a static variable */
    cout << _a++ << endl; /* Output: 3, 3, 3. Will be reinitialized every loop */
    cout << _b++ << endl; /* Output: 3, 4, 5. Will be initialized only once when declared */
}

The static keyword can be used to specify the memory storage characteristics of a variable. A static variable is a variable that is initialized before the code is executed, and it is only declared and initialized once.

The variable _a is a regular local variable. Its memory is still reserved at compile time, but it is not initialized until the code is executed. While the variable _b is initialized before the code is executed. Notice the difference here between the static and non static variable. Since this part of the code is repeatedly executed. Every time the this line is called the variable _a is redeclared and reinitialized to 3.

uint16_t _a = 3; /* lives then dies and gets reinitialized again 2 more times*/

While this line is basically executed only the first time. The value in _b is retained even after the scope of the variable terminates after the loop ends.

static uint16_t _b = 3; /* Declaring a static variable */

This feature is very useful in embedded programming. Say you calculated some sensor error value, you want to retain the value until the next time the code loop runs and then you can update the value, a static variable can help. The other option for keeping a persistent variable is using a global variable.

Global variables are “static” in the sense that they are declared and initialized once. But beware that there is subtle difference between a non-static global variable, a regular global variable, and a static global variable.

extern

The storage specifier extern is used to denote that the variable is declared somewhere else.

#include <cstdint>
#include <iostream>
using namespace std;

extern uint8_t _g_a = 3; /* Variable declared and defined */

int multiplyab(void){
    extern uint8_t _g_b; /* Variable only declared but not defined here */
    return _g_a * _g_b;
}

int main(int argc, char* argv[]) { 
    cout << multiplyab() << endl; /* Output: 15 */
    return 0;
}

uint8_t _g_b = 5; /* At compile time, _g_b is not visible to above functions */
Output:
15

This is useful if the variable is declared in a different file. And you are using the same variable across multiple files, you can't declare the variable more than once in every file, but instead you declare it once in one of the files and tell other files about it by using the extern keyword.

Here, the extern keyword is of no use since by defining the variable _g_a we have specified its value and location.

extern uint8_t _g_a = 3; /* Variable declared and defined */

It comes in handy when you want to use the variable before it is initialized, you just want the compiler to know that “hey” the variable exists. It will use its initialized value even if its initialized later. And this can be seen with the variable _g_b here. Without using extern above the code will not compile.

uint8_t _g_b = 5; /* At compile time, _g_b is not visible to above functions */

Inside the multiplyab function, we just tell the function that there is a global variable named _g_b of uint8_t type. And note until this point the variable has not been declared. It is declared at the end of the file.

const

We discussed the two storage specifiers static and extern. There are two type qualifiers const and volatile. A const variable, as the name suggests, is a constant variable. It’s value cannot be changed. This is very useful when you want to ensure that the value is not changed by mistake. And this can happen, say you have an important variable with a specific value assigned to it. You wouldn’t want the same variable name to be referenced and manipulated somewhere else unintentionally.

#include <cstdint>
#include <iostream>
using namespace std;

int main(int argc, char* argv[]) { 
    const uint8_t a = 1;
    a = 2; /* ERROR: Code will not compile, can't assign new value to const */
    return 0;
}
Output:

error: assignment of read-only variable 'a'

In this example here, we declare const variable a, and then attempt to change it. This code would not compile actually and would give us an error immediately.

const static

We can combine a type qualifier and storage specifier. We can declare a variable to be both const and static. This means that the variable will be declared and initialized once before the code is executed and it cannot be changed.

#include <cstdint>
#include <iostream>
using namespace std;

int main(int argc, char* argv[]) { 
    for (int k = 0; k < 2; k++)
    {
        const static uint8_t _a_s = 4; /* Will only be initialized once, can't be altered */
        cout << (int)_a_s << endl;
    }
    return 0;
}
Output:
4
4

volatile

Sometimes a compiler may optimize a code by assuming that, since a variable is not changed anywhere in the code, its initial value remains the same.

#include <cstdint>
#include <iostream>
using namespace std;
volatile uint8_t _g_a = 20;
uint8_t _g_b = 10;
int main(int argc, char* argv[]) { 
    for (int k = 0; k < 2; k++)
    {
        /* the compiler will not assume the volatile variable doesn't change externally */
        /* _g_a will be retrieved from its address every time its called */
        cout << (int)_g_a << endl; 
        /* The compiler may optimize and set output to 10, assuming it won't change */        
        cout << (int)_g_b << endl; 
    }
    return 0;
}
Output:
20
10
20
10

Well, what if, and we will see this example clearly in microcontroller programming, what if this variable is manipulated by actions outside of the code itself, through some changes in the hardware.

In this case, and to ensure that this non-varying-variable assumption does not damage our program, we can declare a variable to be volatile. This tells the compiler: hey listen, don’t assume the value doesn’t change, every time this variable is called, access and read its latest value from its memory location.

Note that the uint8_t variables are type-casted to int so the output stream treats them as integers not characters.

Next: Operations