Introduction to an Overloaded Term
The static keyword in the C programming language is one of the most overloaded terms in the language. Depending on the context of its use, it can have vastly different meanings. C++ only further complicates the matter with the edition of static member functions and static class variables.
Rather than attempt to generalize some commonality between these vastly different uses of this keyword, today I will try to cover all its many uses along with why to use static (or, in some cases, why to avoid it). I’ll start with the uses common between C and C++, then move into C++-specific uses of the keyword.
If you just want a list or a technical definition, I’d suggest visiting this link for C uses of static and this link for C++ uses.
Local Variables
The static keyword when applied to local variables grants the variable “static” storage duration, meaning the variable will not be allocated on the stack and will not go out of scope
Example
If we called the following function 3 times, what would the code print?
void f() {
int i = 0;
i++;
printf("%d\n", i);
}
The answer is:
1
1
1
The variable i goes out of scope with each invocation of the function f, so it will always print 1. This is because local variables like i reside on the stack, meaning once i goes out of scope at the end of the function it gets popped off the stack and our storage is gone. Additionally, i gets initialized to 0 with each call to f.
Now if we were to make i static as follows:
void f() {
static int i; // = 0; not necessary, statics are initialized to 0 by default.
i++;
printf("%d\n", i);
}
Calling the function f 3 times would now print:
1
2
3
i only gets initialized once to 0, and it gets incremented with each call. Our variable’s storage is no longer on the stack—instead, it resides in a special part of memory called the data segment.
Usage
Most likely, this is the first usage of static one may learn in an introductory C programming course. This is unfortunate, because you should almost never use this keyword on local variables.
The only time I would use this keyword in production code is if I’m being lazy for something non-essential or if I’m debugging something. For instance, what I do a lot is something like the following:
void mySuperComplexFunction(MagicalData *a, ScientificData *b) {
// Lots of code...
static int number_of_calls = 0;
number_of_calls++;
printf("Function was called %d times\n", number_of_calls);
static bool once = false;
if (!once) {
printTonsOfDiagnosticInfo(a, b);
once = true;
}
}
So it’s super handy for debugging, but let’s zoom in on a specific issue I’ve had in the past. A co-worker of mine decided to use static deep inside of code which parsed NMEA messages to represent a sentence index. This worked well for a year, but eventually we had to re-use this object in a context which required us to destroy and then re-initialize the object that parsed GPS messages.
Now, because the index was declared static, this caused our code to segfault because the index was still allocated from the last time the object was created. Even though we’d created a new object, the static variable was not re-initialized by re-creating the object.
This is the main thing to look out for: oftentimes, if declaring a variable static seems to make things work, you might really want to make the variable a class member or something instead. Otherwise, using static variables within the method can cause residual state to mess up subsequent re-uses of the same code as there is no easy way to re-initialize the data (as show in my previous real-world example)
There’s also the classic example of old-style C functions like localtime. See this note from man 3 localtime:
The return value points to a statically allocated struct which might be overwritten by subsequent calls to any of the date and time functions. The
localtime_r()function does the same, but stores the data in a user-supplied struct.— man 3 localtime
So localtime was designed to return a pointer to a struct tm object statically allocated, yet it was such a buggy, error-prone API that POSIX stepped up and added a localtime_r function as a band-aid to the problem (there are many other *_r functions to address similar issues elsewhere). Remember this example before you ever try returning a buffer allocated with static to the caller (better hope your program isn’t multi-threaded!).
But before I write off this usage of static entirely, here’s an interesting C++ example where it can come in handy:
static SingletonClass & get()
{
static SingletonClass s;
return s;
}
If you need to use the singleton pattern anywhere, this is a reasonable way of storing and accessing the singleton. It avoids the initialization order issues that plague creating class objects at the global scope because now the singleton, which is still accessible from anywhere with access to the get() function, will be initialized at the first call to get().
If making a proper singleton you’d want to also delete the copy constructor & move constructor and stuff, but this is just a rough example.
Global Variables & Functions
In the context of global variables and functions, the static keyword is a valuable tool that can limit the visibility of terms to the target source file.
Example
Not everybody learns this in an introductory C class because it’s typically a terrible idea, but any global variable or function can be used even if it’s in another C file (and not in a header file) using the extern keyword. Here’s an example:
/* test.c */
const int MY_CONSTANT = 1024;
void myFunction() {
printf("Hello there\n");
}
/* main.c */
int main() {
extern const int MY_CONSTANT;
extern void myFunction();
myFunction();
printf("%d\n", MY_CONSTANT);
}
The static keyword allows you to restrict this behavior. extern and static (in this context) are both what C calls storage specifiers. In C, all variables and functions are extern by default, which allows the linker to find the MY_CONSTANT and myFunction symbols. If we make both the variable and function in test.c static as follows:
static const int MY_CONSTANT = 1024;
static void myFunction() {
printf("Hello there\n");
}
Then we get these errors from the linker when trying to compile the code:
/usr/bin/ld: /tmp/ccKBnre6.o: warning: relocation against `MY_CONSTANT' in read-only section `.text'
/usr/bin/ld: /tmp/ccKBnre6.o: in function `main':
main.c:(.text+0x5): undefined reference to `myFunction'
/usr/bin/ld: main.c:(.text+0xb): undefined reference to `MY_CONSTANT'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status
/usr/bin/ld is our linker, but it can no longer find the symbols now that they’re declared static. We might naively try to do this:
int main() {
static extern void myFunction();
static extern const int MY_CONSTANT;
myFunction();
printf("%d\n", MY_CONSTANT);
}
But then we’d get these predictable compile errors:
main.c: In function ‘main’:
main.c:3:5: error: multiple storage classes in declaration specifiers
3 | static extern void myFunction();
| ^~~~~~
main.c:3:24: error: invalid storage class for function ‘myFunction’
3 | static extern void myFunction();
| ^~~~~~~~~~
main.c:4:5: error: multiple storage classes in declaration specifiers
4 | static extern const int MY_CONSTANT;
| ^~~~~~
main.c:5:5: error: implicit declaration of function ‘myFunction’ [-Wimplicit-function-declaration]
5 | myFunction();
| ^~~~~~~~~~
A function cannot be declared static at local function scope (it makes no sense as there’s no way the linker could ever resolve it) and we can’t give a variable or function two storage classes—in this case, we’re trying to declare it static and extern at the same time!
Usage
If you’re not accessing a global variable or function from an external file (through either a header file or through the extern keyword), the symbol should always be declared static.
To those familiar with C++, static in this meaning may be thought of as making a variable or function private in a C++ class. Just as a private variable is only accessible within a class it is declared, a static global variable is only accessible within the file it is declared.
In a similar sense, extern corresponds with C++’s public keyword. So if you’re stuck with C but want some object oriented encapsulation, these static and extern at the file level can be used as a poor man’s private and public.
Array Declaration
Now this is a strange one I had to research to write this article. It is specific to C, added with the C99 standard. I used this page as reference.
Example
So apparently you can write this:
void myFunction(int array[static 100]) {
// myFunction implementation
}
int main()
{
int *ptr = NULL;
myFunction(ptr); // undefined behavior
int array[100] = {0};
myFunction(array); // good
}
Usage
But what does static do here? Apparently, it tells the compiler that the array will be of at least the specified length. And, it also guarantees that the array cannot be NULL.
Hopefully language tools like clangd produce errors when passing NULL in this context, otherwise this can easily be a foot gun for causing undefined behavior.
So while this is a good tool to use in that it should increase the clarity of your program, its relative obscurity and applicability only to C and not C++ limits its usefulness significantly.
And, if you want to limit the size of an array, it’s probably best to just pass in the size as a separate variable:
void myFunction(int *array, size_t size) {
// myFunction implementation
}
Or use std::span if in C++:
void myFunction(std::span<int> array) {
// myFunction implementation
}
Class Variables
In C++, a static variable defined within a class is shared between all instances of a class.
Example
Let’s say I’m making a game, and I want every single object in the game—enemies, characters, and even rocks—to have a unique ID number. If we have some sort of IGameEntity class, a simple way of doing this could be as follows:
class IGameEntity
{
public:
IGameEntity();
private:
static uint64_t idGenerator = 0;
uint64_t id = 0;
}
IGameEntity::IGameEntity() : id(idGenerator++) {}
Now, since idGenerator is only allocated once—and is shared between all instances of the IGameEntity class—each time we create a new IGameEntity it gets a new id.
Usage
The static keyword for class variables isn’t immensely useful. It can be used for patterns like I’ve described above, but that’s close to the extent of it.
static is used to share state between all instances of a class, but I personally have not run into many instances where this pattern is useful.
It’s often used to define constants since there’s no point in having multiple copies of a constant in your program:
class IGameEntity
{
public:
static constexpr int THE_PRICE_OF_MAPLE_SYRUP_AT_ALDI = 5; // At least I hope this remains constant...
}
Your variable becomes implicitly inline here, which guarantees only a single definition in your entire program that is shared between instances of the class.
Class Functions
A static function defined within a class is able to be called without an instance of the class it’s defined in. As a result, it is unable to access any non-static class variables or call any non-static class functions. To those familiar to functional programming, you can almost think of it as a poor man’s pure function.
Example
A function declared as such:
struct MyStupidClass {
static void printStupidStuff() { std::printf("Something dumb\n"); }
}
Can be called with (or without) an instance of the class created:
// works, without an instance of the class
MyStupidClass::printStupidStuff();
// also works, with an instance of the class
MyStupidClass c;
c.printStupidStuff();
Now, note what happens if we do the following:
struct MyBrokenClass {
int x = 0;
static void modifyStuff() { x = 5; }
};
int main() {
MyBrokenClass::modifyStuff();
}
See what’s wrong? This code will fail to compile with the following output:
$ g++ test.cpp
test.cpp: In static member function ‘static void MyBrokenClass::modifyStuff()’:
test.cpp:3:33: error: invalid use of member ‘MyBrokenClass::x’ in static member function
3 | static void modifyStuff() { x = 5; }
| ^
test.cpp:2:9: note: declared here
2 | int x = 0;
| ^
As per our previous definition, a member function declared static cannot use any non-static member variables or non-static member functions.
Usage
This is an immensely useful tool to make functions easier to understand.
For example, if you’re working in a very large class with dozens of member functions and member variables, it can become difficult for a new programmer looking at the code to grasp where variables are modified. By marking a member function static, you tell programmers looking at your code that this function has no side-effects on the state of the class—meaning, any variables tracking some state in the function cannot be manipulated.
Conversely, if a codebase is strict about marking class member functions static whenever possible, you can imply the reverse when a function is not declared static.
static for this use-case is far from perfect though. The main reason is that you can still manipulate state if it’s not a part of the class. For instance, if you’re writing LVGL code in C++—which is a C library—you can call functions like lv_cont_create(NULL, NULL); inside a static class function, which definitely manipulates state.
Actual Pure Functions in C & C++
I don’t have any experience using them, but it appears the proper way to actually specify function purity is to use compiler-specific attributes. Here’s an example:
// https://gcc.gnu.org/onlinedocs/gcc/Common-Attributes.html#index-pure
int hash (char *) __attribute__ ((pure));
// https://gcc.gnu.org/onlinedocs/gcc/Common-Attributes.html#index-const
[[gnu::const]] int square (int);
Check out this link for more details.
Conclusion
Mastering usage of the static keyword is a vital part of writing good C or C++. In particular, understanding static functions and static class functions are essential skills that can lead to greatly improved code.
I just wish the static keyword didn’t have so many different meanings…