С interview questions. Static.
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
andD
contain initialized variables, we getd
if they are static andD
otherwise. The section in this case will most likely be.data
For uninitialized global variables, we get
b
if they are static andB
orC
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 keywordextern
inside this.c
file without a definition - The classes
t
andT
indicate the code that is defined; the difference betweent
andT
is whether the function is local (t
) in the file or not (T
), i.e. whether the function was declared asstatic
. - Classes
D
contain initialized global variables. At the same time, static variables belong to thed
class. - For uninitialized global variables, we get
b
if they are static andB
orC
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
...