If static is about hiding things within a file, extern is about sharing things across files. It’s the keyword that lets you say “this variable or function exists, but it’s defined somewhere else.”
Once you understand the difference between a declaration and a definition, extern becomes much clearer.
Declarations vs Definitions
In C, a declaration tells the compiler “this thing exists and has this type.” A definition actually creates the thing and allocates storage for it.
For functions, the distinction is obvious:
// Declaration: tells the compiler the function exists
int add(int a, int b);
// Definition: actually implements the function
int add(int a, int b) {
return a + b;
}For variables, it’s a bit more subtle, and that’s where extern comes in.
Extern Variables
Let’s say you have a global variable in one file that you want to use in another file.
// config.c
int max_connections = 100; // This is a definitionYou’ve defined max_connections here. Memory is allocated for it, and it’s initialised to 100.
Now, in another file, you want to use this variable:
// server.c
extern int max_connections; // This is a declaration
void setup_server(void) {
printf("Max connections: %d\n", max_connections);
}The extern keyword tells the compiler “there’s an int called max_connections somewhere, but not here. Don’t allocate storage for it. Just trust me that it exists, and the linker will sort it out.”
Without extern, you’d get a problem. If you wrote:
// server.c
int max_connections; // This is another definition!Now you’ve got two definitions of the same variable in two different files. The linker will complain about multiple definitions (or worse, with some linkers and settings, it might silently merge them in ways you don’t expect).
The Header File Pattern
In practice, you almost always see extern declarations in header files. This is the standard pattern:
// config.h
#ifndef CONFIG_H
#define CONFIG_H
extern int max_connections; // Declaration
extern const char *app_name; // Declaration
#endif// config.c
#include "config.h"
int max_connections = 100; // Definition
const char *app_name = "MyApp"; // Definition// server.c
#include "config.h"
void setup_server(void) {
// We can use max_connections and app_name here
printf("%s allows %d connections\n", app_name, max_connections);
}Any file that includes config.h can use these variables. The actual storage lives in config.c, and everyone else just references it through the extern declarations.
NOTEThe reason this pattern works is down to how C handles compilation and linking as separate stages. When the compiler processes
server.c, it sees#include "config.h", which pastes in theextern int max_connections;declaration. This tells the compiler “there’s an integer calledmax_connectionssomewhere, and I need to reference it.” The compiler doesn’t know or care where it actually lives — it just generates code that refers to a symbol calledmax_connectionsand trusts that the linker will resolve it later. Meanwhile, when the compiler processesconfig.c, it seesint max_connections = 100;, which is a definition. This actually allocates four bytes of storage, initialises them to 100, and exports the symbolmax_connectionsso the linker can find it. When you link the object files together, the linker sees thatserver.oneeds a symbol calledmax_connections, andconfig.oprovides one, so it wires them up. Every reference tomax_connectionsin your entire program now points to that single four-byte location defined inconfig.c. The header file is just a convenient way to share the declaration across multiple source files — without it, you’d have to manually writeextern int max_connections;at the top of every file that needs access to it, which would be tedious and error-prone.
This is the C way of having shared global state. Whether you should have shared global state is a different question, but when you need it, this is how you do it.
Extern With Functions (Usually Unnecessary)
Here’s something that catches people out: function declarations are implicitly extern.
// These two lines mean exactly the same thing
int add(int a, int b);
extern int add(int a, int b);The extern keyword on a function declaration is redundant. You’ll see it occasionally in older code or in code written by people coming from other languages, but it’s not doing anything. Most style guides recommend leaving it off.
This is why function prototypes in header files don’t need extern:
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b); // extern is implied
int multiply(int a, int b); // extern is implied
#endifVariables are different. Without extern, a variable declaration at file scope is a definition (or at least a tentative definition, but let’s not go down that rabbit hole just yet).
Extern With Initialisers = That’s a Definition
If you try to initialise an extern variable, you’re actually defining it:
extern int max_connections = 100; // This is a definition, despite extern!Some compilers will warn about this. The extern keyword is essentially being ignored because you’re providing an initial value, which means you’re defining the variable right here.
TIPWhether the compiler warns you or not, this is a source of confusion. One you can do without by being explicit: use
externfor declarations (no initialiser), and ensure you leave it off for definitions (with or without an initialiser).
A Practical Example: Building a Simple Module
Let’s put this together with a small example. Say you’re writing a logging module.
// logger.h
#ifndef LOGGER_H
#define LOGGER_H
// External variable: current log level
extern int log_level;
// Log level constants
#define LOG_ERROR 0
#define LOG_WARN 1
#define LOG_INFO 2
#define LOG_DEBUG 3
// Function declarations (extern is implied, so we don't write it)
void log_message(int level, const char *message);
void set_log_level(int level);
#endif// logger.c
#include <stdio.h>
#include "logger.h"
// Definition of the log level variable
int log_level = LOG_INFO; // Default to INFO
void log_message(int level, const char *message) {
if (level <= log_level) {
const char *level_names[] = {"ERROR", "WARN", "INFO", "DEBUG"};
printf("[%s] %s\n", level_names[level], message);
}
}
void set_log_level(int level) {
log_level = level;
}// main.c
#include "logger.h"
int main(void) {
log_message(LOG_INFO, "Starting up");
// We can access log_level directly because of the extern declaration
if (log_level >= LOG_DEBUG) {
log_message(LOG_DEBUG, "Debug mode is on");
}
set_log_level(LOG_DEBUG);
log_message(LOG_DEBUG, "Now this will print");
return 0;
}The extern int log_level; in the header lets any file that includes logger.h read (or modify) the log level. The actual variable lives in logger.c.
NOTEDid you spot any issues with this code example?
This example is fine for demonstrating the extern pattern, but there are a few things you’d want to improve in production code:
- Exposing log_level directly via extern means any file can modify it without going through set_log_level(), which undermines encapsulation, if you later need to add validation or trigger behaviour when the level changes, you’d have to track down every place that touches the variable directly. Better to make log_level static in logger.c and add a get_log_level() function if external code needs to read it.
- The #define constants would be cleaner as an enum, which gives you better type safety and more useful information in a debugger.
- There’s no bounds checking when indexing into level_names[], if someone passes an invalid level, you’re into undefined behaviour territory.
- The level_names array is declared inside the function, which means it’s technically recreated on each call (though the compiler will likely optimise this away). Declaring it as static const at file scope makes the intent explicit and guarantees single initialisation.
None of these are critical for an example on the extern keyword, but they’re worth being aware of as you move toward real-world code.
Extern and Static
It’s worth noting that extern and static are opposites when applied to global variables and functions.
static means internal linkage: visible only within this file.
extern means external linkage: visible across files, defined elsewhere.
You can’t use both on the same declaration. If you write extern static int x;, the compiler will rightly tell you that makes no sense.
Wrapping Up
The extern keyword is fundamentally about separation: declaring something in one place and defining it in another. It’s the glue that lets multiple C files share variables.
The key points to remember are that extern on a variable means “this is a declaration, not a definition.” On a function, it’s redundant since functions are implicitly extern already. And somewhere in your program, every extern variable needs an actual definition without extern.
If you’re structuring a C project with multiple files, you’ll use this pattern constantly. Declarations with extern go in headers, definitions go in source files, and the linker ties it all together.
As always, thanks for joining me on this journey into embedded development!
