С interview questions. Static.

Aliaksandr Kavalchuk
5 min readJul 22, 2023

So one of the most common questions at interviews on the knowledge of the C language is what the keyword static mean and how is it used?

The keyword static is used for two things: extend and limit. Extend the lifetime of a local variable and limit the scope of the variable and function.

Lifetime extension

Local (auto) variables, i.e. variables that you declare inside the function body and do not add the magic word static to the declaration, are stored either in the processor registers or on the stack when calling the function. And since there are usually not so many registers and stacks, and everyone needs these things, then since the function completes, your local variables stop being valid. They are not physically destroyed, but you can no longer use them because they will be legally overwritten by some other function. I.e. in fact, your local variables "live" only while the processor executes commands inside your function. But what should you do if you need to save the values of some variables between calls to your function? Well, you can declare these variables outside the function body, or you can add the keyword static to the declaration of such variables.

uint16_t time_counter(void)
{
static uint16_t counter = 0;

....
}

If adding static when declaring a local variable, it will no longer be stored on the stack and, accordingly, "destroyed" after the function is completed. This variable will be stored in RAM, in the .data or .bss section. I.e. it will behave almost exactly the same as if you defined this variable not inside the function, but outside the function body.

Look at the function:

uint16_t static_example_func(void)
{
uint16_t local_var = 0;
uint16_t local_init_var = 10;
static uint16_t static_local_var = 0;
static uint16_t static_init_local_var = 10;


local_var++;
static_local_var += local_var;

return static_local_var;
}

Compile the code of this function and use the nm utility to look at the information about the symbols of the object file:

00000001 T main
U memset
00000001 t static_example_func.0
00000000 d static_init_local_var.1
00000000 b static_local_var.2
00000001 T SystemClock_Config

As you can see, the object file contains information only about the static variables static_init_local_var and static_local_var and they are assigned, different classes: d and b respectively.

Classes d and D contain initialized variables, we get d if they are static and D otherwise. The section in this case will most likely be .data

For uninitialized global variables, we get b if they are static and B or C otherwise. The section in this case will most likely be .bss or *COM*

There is no information about local variables in the object file because these variables are allocated and initialized automatically on the stack each time when a function is entered by a special assembler code generated by the compiler:

uint16_t static_example_func(void)
{
...
uint16_t local_var = 0;
80004d6: 2300 movs r3, #0
80004d8: 81fb strh r3, [r7, #14]

uint16_t local_init_var = 10;
80004da: 230a movs r3, #10
80004dc: 81bb strh r3, [r7, #12]
....
}

Scope limitation

The next thing that the keyword static is used for is to limit the scope of a variable or function. I.e. by adding static to declare a variable or function, we make it so that we can access this variable and function only from the code that is in the same file (translation unit). The mechanism is the same for both the variable and the function, but we will now consider how exactly it works.

And this mechanism works due to the presence of a connection between the declaration and definition of a variable or function.

A definition binds the name to the implementation, which can be either code or data: the definition of a variable prompts the compiler to reserve some memory area, possibly setting it some specific value; the definition of a function forces the compiler to generate code for this function

A declaration is a promise that the definition exists somewhere else in the program. Wherever the code refers to a variable or function, the compiler allows this only if it has seen the declaration of this variable or function before.

In the previous section, when looking at the object file, it was seen that the compiler assigns different classes to symbols. Then we worked with only two classes: d and b. But there are more of them:

  • The U class means undefined references. These classes are assigned to variables or functions that have a declaration with the keyword extern inside this .c file without a definition
  • The classes t and T indicate the code that is defined; the difference between t and T is whether the function is local (t) in the file or not (T), i.e. whether the function was declared as static.
  • Classes D contain initialized global variables. At the same time, static variables belong to the d class.
  • For uninitialized global variables, we get b if they are static and B or C otherwise.

The declaration and definition have been sorted out now let’s deal with binding. Binding is the relationship of the name of an object (an object here means a variable or function) with a specific implementation of this object. The work on such binding is carried out by a linker and there are two types of binding: internal or external.

With internal binding, the symbol is visible to the linker only inside this translation unit. Visibility means that the linker will be able to use this symbol only when processing the translation unit in which the symbol was declared, and not later (as in the case of symbols with an external link). If an object or function has an external binding, then the linker will be able to see it when processing other translation units. Using the keyword static gives the symbol an internal binding. The keyword extern gives an external binding.

Thus, as mentioned above, by adding static when defining a variable and a function, we make it so that the linker sees this variable and function only when the translation unit in which the variable and function are defined is processed. And you will not be able to access (by normal means) a variable or a function declared as static from other translation units. Even if you add a static variable or function into the header file and include this file in several .c files, each translation unit that includes this file will receive a unique copy of this symbol. This means that the compiler will literally allocate a completely new, unique copy for each translation unit, which, obviously, can lead to high memory consumption.
Let's show this with an example.

header.h:

static int variable = 42;

file1.h:

void function1();

file2.h:

void function2();

file1.c:

#include "header.hpp"

void function1() { variable = 10; }

file2.c:

#include "header.hpp"

void function2() { variable = 123; }

main.c:

#include "header.h"
#include "file1.h"
#include "file2.h"

int main(void)
{
function1();
function2();
}

Each translation unit including a header.h gets a unique copy of the variable, due to the fact that it has an internal connection.

Let’s make sure of this by looking into the .map file obtained after compilation and see the presence of three variable variables located at three different addresses:

...
*(.data)
*(.data*)
.data.variable
0x0000000020000000 0x4 ./Core/Src/file1.o
.data.variable
0x0000000020000004 0x4 ./Core/Src/file2.o
.data.variable
0x0000000020000008 0x4 ./Core/Src/main.o
...
Thanks for the support — https://www.buymeacoffee.com/zamuhrishka

--

--