The Three Jobs of the Static Keyword in C

7 min

The static keyword in C is one of those things that trips people up because it does different things depending on where you use it. Same keyword, different behaviour. Once you know the three contexts, though, it becomes much more straightforward.

1. Static Local Variables

Normally, when a function finishes executing, its local variables disappear. The stack frame gets cleaned up, and those variables cease to exist. Next time you call the function, you start fresh.

A static local variable changes that. It persists between function calls.

#include <stdio.h>

void count_calls(void) {
    static int call_count = 0;  // Initialised once, persists forever
    call_count++;
    printf("This function has been called %d times\n", call_count);
}

int main(void) {
    count_calls();  // Prints: 1
    count_calls();  // Prints: 2
    count_calls();  // Prints: 3
    return 0;
}

The key thing here is that call_count is initialised to zero only once, when the program starts. After that, it just keeps its value between calls. It’s not recreated each time the function runs.

Where does it live in memory? Not on the stack. Static local variables are stored in the data segment (or BSS segment if uninitialised), alongside global variables. They have the lifetime of the entire program, but the scope of the function they’re declared in.

I find this useful for things like generating unique IDs, caching expensive calculations, or tracking state in a function that needs to remember what happened last time.

One thing to watch out for: this can make your functions harder to reason about and test, because they’re no longer pure functions. The output depends on how many times you’ve called them before. Use it when you need it, but be aware of the trade-off.

NOTE

A pure function is one that always produces the same output for the same input, and has no side effects. If you call add(2, 3), you get 5 every single time, regardless of when you call it, how many times you’ve called it before, or what else is happening in your program. The function doesn’t have static variables, doesn’t read from global variables, doesn’t modify anything outside its own scope, and doesn’t do I/O. It just takes its arguments, computes a result, and returns it. This makes pure functions easy to test (no setup required, just check input against output), easy to reason about (you can understand them in isolation), and safe to call from anywhere without worrying about hidden consequences.

The moment a function reads from or writes to a static variable, accesses global state, prints to the console, or modifies something via a pointer that was passed in, it stops being pure. That’s not always a bad thing, programs need side effects to do anything useful, but knowing which of your functions are pure helps you understand where complexity and unpredictability live in your codebase.

2. Static Global Variables

When you declare a global variable (outside any function) as static, you’re saying “this variable should only be visible within this file.”

Without static:

// file1.c
int shared_counter = 0;  // Visible to other files via extern
// file2.c
extern int shared_counter;  // We can access it from here

With static:

// file1.c
static int private_counter = 0;  // Only visible in file1.c
// file2.c
extern int private_counter;  // Linker error! Can't see it.

This is about controlling visibility across translation units. The static keyword gives the variable internal linkage, meaning it’s private to that file.

NOTE

A translation unit is what the C compiler actually sees after the preprocessor has finished its work. When you compile a .c file, the preprocessor first handles all the #include directives, macro expansions, and conditional compilation blocks, producing a single blob of source code. That blob, your original .c file with all the headers inlined and macros expanded, is one translation unit.

The compiler processes each translation unit independently, turning it into an object file (.o or .obj), and then the linker combines all the object files into your final executable. This is why static functions and variables are “file-private,” they’re actually private to the translation unit, which in practice means the .c file they’re defined in (plus whatever headers that file includes). It’s also why you can have the same static function name in two different .c files without a conflict: each lives in its own translation unit, and the linker never sees them because they have internal linkage. When people say “file scope” in C, they really mean “translation unit scope,” though in most cases the distinction doesn’t matter since each .c file typically becomes one translation unit.

Why would you want this? Encapsulation. If you’re writing a module and you have some state that only that module should touch, making it static prevents other parts of the codebase from accidentally (or deliberately) messing with it. It’s a bit like making a member private in an object-oriented language.

3. Static Functions

The same logic applies to functions. A static function is only visible within the file where it’s defined.

// utils.c

// This helper is only for internal use within utils.c
static int calculate_checksum(const char *data, size_t len) {
    int sum = 0;
    for (size_t i = 0; i < len; i++) {
        sum += data[i];
    }
    return sum;
}

// This is the public function other files can call
int validate_data(const char *data, size_t len) {
    int checksum = calculate_checksum(data, len);
    return checksum == expected_checksum;
}

Other files can call validate_data, but they can’t call calculate_checksum directly. It’s an implementation detail.

This is genuinely useful for keeping your API clean. When someone includes your header file, they shouldn’t see every little helper function you’ve written. They should see the functions you’ve designed for them to use. The static keyword lets you hide the rest.

It also means you can have static functions with the same name in different files without causing linker conflicts. Each one is private to its own file.

A Quick Summary

Here’s how I think about it:

  • Static local variable: “Keep your value between calls, but stay scoped to this function.”
  • Static global variable: “Exist for the whole program, but only be visible in this file.”
  • Static function: “Only be callable from within this file.”

The common thread is persistence or restriction of visibility. For local variables, it’s about persistence. For globals and functions, it’s about restricting visibility to the current translation unit.

One More Thing: Static in Headers

You might occasionally see static used in header files, often with inline functions.

// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// static inline: asking the compiler to insert this code directly
// at each call site, avoiding function call overhead
static inline int max(int a, int b) {
    return (a > b) ? a : b;
}

static inline int min(int a, int b) {
    return (a < b) ? a : b;
}

static inline int clamp(int value, int lower, int upper) {
    // Constrain a value to a range
    if (value < lower) return lower;
    if (value > upper) return upper;
    return value;
}

#endif

This creates a separate copy of the function in each translation unit that includes the header. Sometimes that’s what you want (for small inline functions), but it can bloat your binary if you’re not careful.

TIP

If you see static inline in a header, that’s usually deliberate. If you see just static for a regular function in a header, someone might have made a mistake. With static inline, the intent is clear: you want the function inlined, and the static is there as a fallback to prevent linker errors if inlining doesn’t happen. But with just static on a substantial function, you’re not asking for inlining at all, you’re just creating a separate copy of the function in every translation unit that includes the header, and that’s almost never what someone actually wants.

Wrapping Up

The static keyword is doing a lot of work in C. Same word, different contexts, different effects. Once you’ve got the three uses clear in your head, you’ll find it’s a handy tool for managing state and keeping your code organised.

If you’re coming from a language like Java or C#, note that C’s static has nothing to do with class members (C doesn’t have classes). It’s all about storage duration and linkage. Don’t let the shared terminology confuse you.

As always, thanks for joining me on this journey into embedded development!