Code of the Day
AdvancedC compilation model

static and extern

Control symbol visibility across C translation units — using static to restrict visibility to one file and extern to share across files.

CAdvanced10 min read
By the end of this lesson you will be able to:
  • Use static on a function or variable to restrict it to one translation unit
  • Use extern to declare a variable that is defined in another source file
  • Explain the difference between internal and external linkage
  • Apply the one-definition rule to avoid duplicate symbol errors

When a C program is built from multiple source files, the linker connects symbols (function and variable names) across them. The static and extern keywords control which symbols are visible to the linker and which are hidden within a single file.

Linkage

Every function and file-scope variable in C has linkage — a property that determines whether the symbol can be referenced from other translation units.

  • External linkage (the default for functions and non-static globals): the symbol is visible to the linker and can be referenced from any object file.
  • Internal linkage (static applied at file scope): the symbol is only visible within the current translation unit.
  • No linkage (local variables): the symbol cannot be referenced from outside the block where it is declared.

static at file scope

static applied to a function or global variable restricts it to the current .c file:

/* utils.c */

static int helper(int x) { /* internal linkage -- invisible to other files */
    return x * 2;
}

int public_function(int x) { /* external linkage -- visible to other files */
    return helper(x) + 1;
}

static int file_counter = 0; /* internal -- invisible to other files */

Use static to:

  • Hide implementation details that should not be part of the public API.
  • Avoid name collisions: two files can each have a static int count = 0; without interfering.
  • Signal to readers that a function is local — it is not called from outside this file.

This is C's version of "private" — not enforced by a type system, but enforced by the linker.

extern — declaring without defining

extern declares that a variable is defined in some other translation unit:

/* config.c */
int debug_level = 0; /* DEFINITION: allocates storage */

/* main.c */
extern int debug_level; /* DECLARATION: just a promise; no storage */

The extern declaration tells the compiler the type of debug_level without allocating it. The linker resolves the reference to the definition in config.c.

In practice, put the extern declaration in a header:

/* config.h */
extern int debug_level;

/* config.c */
#include "config.h"
int debug_level = 0; /* definition */

/* main.c */
#include "config.h"
/* now debug_level is declared and usable */

The one-definition rule

Each symbol with external linkage must be defined exactly once across all translation units. Multiple definitions produce a linker error. Multiple declarations (with extern) are fine.

/* a.c */
int shared = 10; /* definition */

/* b.c */
int shared = 20; /* ERROR: multiple definition of 'shared' */

The most common accidental violation: putting a variable definition (without extern) in a header file. When two .c files include that header, both define the variable, causing a linker conflict.

static inside functions — a recap

Within a function body, static changes the variable's lifetime (persists between calls) while keeping its scope local. This is different from file-scope static (which affects linkage):

void count_calls(void) {
    static int count = 0; /* local scope, static lifetime */
    count++;
    printf("Called %d times\n", count);
}

extern "C" for C++ interoperability

When linking C code with C++, extern "C" prevents C++ from mangling the function name:

/* In a header used by both C and C++ */
#ifdef __cplusplus
extern "C" {
#endif

void my_c_function(int x);

#ifdef __cplusplus
}
#endif

Without this, a C++ compiler mangles my_c_function to something like _Z16my_c_functioni, and the linker cannot match it to the C definition.

Make everything static by default. A good practice: start with all functions and variables as static. Only remove static when you explicitly need a symbol to be visible outside the file. This minimises the public API surface, reduces naming conflicts, and makes it clear which functions are implementation details.

Where to go next

Next: Makefiles and build systems — automating the compilation of multi-file projects so you only recompile what has changed.

Finished reading? Mark it complete to track your progress.

On this page