static and extern
Control symbol visibility across C translation units — using static to restrict visibility to one file and extern to share across files.
- 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 (
staticapplied 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
}
#endifWithout 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.
Header files and include guards
Write proper C header files — declarations vs. definitions, include guards, forward declarations, and the include search path.
Makefiles and build systems
Automate C project builds with make — writing Makefile rules, using pattern rules and variables, and only recompiling changed files.