C / MULTI-FILE PROGRAMS AND BUILDS
Internal versus external linkage with static and extern
Decide which file-scope names other translation units can see, using static for internal linkage and extern to declare names defined elsewhere.
What you will learn
- Add static to every file-scope name no other translation unit needs
- Keep one definition in one .c file and an extern declaration in the header
- Read nm output: T/D are exported symbols, t/d are file-private ones
- Distinguish static at file scope (internal linkage) from static in a block (no linkage)
Understanding Internal versus external linkage with static and extern
The compiler processes one translation unit at a time and writes an object file containing a symbol table. Only names with external linkage are offered to the linker as candidates for matching across object files; a name with internal linkage is recorded as a local symbol that nothing outside its own translation unit can bind to. Identifiers declared inside a block have no linkage at all, so two functions may each have a variable called i with no relationship between them. Linkage is therefore a question about names and the linker, not about where bytes live or how long they live.
At file scope, an identifier declared without a storage-class specifier gets external linkage, which is why a stray int config; in a header becomes a definition in every file that includes it. Writing static at file scope switches that identifier to internal linkage, so the same spelling in another .c file names a completely different object or function. Writing extern on a declaration says the opposite: this name has external linkage and its single definition lives somewhere the linker will find, so no storage is reserved here. Function declarations already default to external linkage, which is why extern on a prototype changes nothing.
A useful mental model is that each .c file has a public surface and a private interior, and static is the tool that shrinks the surface. Hiding a name does not hide the memory: a static object's address can still be handed to another file through a non-static function, and that file can read and write through the pointer even though it cannot spell the name. The two classic linker messages map straight onto this model: undefined reference means something was declared external but no external definition exists, often because the definition was accidentally marked static, and multiple definition means two translation units each supplied an external definition of the same name.
<stdio.h>
/* No storage class at file scope: external linkage, visible to the linker. */
int total = 0;
/* static at file scope: internal linkage, private to this translation unit. */
static int calls = 0;
/* A static function: other .c files cannot call it even if they declare it. */
static void bump(int amount)
{
calls++;
total += amount;
}
/* The exported entry point: this is the only name other files should use. */
void add_twice(int amount)
{
bump(amount);
bump(amount);
}
int main(void)
{
extern int total; /* a declaration, not a new object: same total as above */
add_twice(5);
add_twice(1);
printf("total = %d\n", total);
printf("bump called %d times\n", calls);
return 0;
}
static and extern decide which names the linker can match across translation units; they are about name visibility, not about storage location or lifetime.
Worked examples
extern declares, it does not define
Shows that an extern declaration reserves no storage and is satisfied by a definition appearing later.
<stdio.h>
extern int limit; /* declaration: promises a definition exists */
static int scale; /* definition, internal linkage, zero initialised */
static int scaled(int v)
{
return v * scale;
}
int limit = 40; /* the definition that satisfies the declaration above */
int main(void)
{
scale = 3;
printf("scaled(10) = %d\n", scaled(10));
printf("limit = %d\n", limit);
printf("clamped = %d\n", scaled(10) > limit ? limit : scaled(10));
return 0;
}
Example explained
Line 1extern int limit; allocates nothing; it only tells the compiler the name exists with external linkage.
Line 2int limit = 40; is the actual definition, and here it happens to be in the same file, so the linker never has to look further.
Line 3static int scale; is both a definition and a promise that no other file can refer to this object by name.
Line 4static int scaled(...) keeps the function name out of the exported symbols, so another file may define its own scaled without a clash.
extern after static keeps internal linkage
Demonstrates that a later extern declaration inherits the internal linkage of the earlier static declaration.
<stdio.h>
static int registry_size = 0; /* internal linkage */
extern int registry_size; /* same object, linkage unchanged */
static const char *names[4];
int register_name(const char *name)
{
if (registry_size == 4)
return -1;
names[registry_size] = name;
return registry_size++;
}
int main(void)
{
printf("slot %d\n", register_name("clk"));
printf("slot %d\n", register_name("rst"));
printf("names[0] = %s\n", names[0]);
printf("registry_size = %d\n", registry_size);
return 0;
}
Example explained
Line 1The second declaration uses extern but does not promote registry_size to external linkage: when a prior declaration in the same unit specified linkage, extern adopts it.
Line 2So registry_size is still invisible to other files, and register_name is the only way for them to touch the registry.
Line 3names is static, so its four pointers start as null pointers and no other file can index the array directly.
Line 4register_name has external linkage by default, which is what makes it the intended public API of this file.
static in a block means something else
Contrasts static at file scope, which sets linkage, with static inside a function, where the identifier has no linkage.
<stdio.h>
static int file_scope_calls = 0; /* internal linkage, one per .c file */
int next_id(void)
{
static int local_counter = 0; /* no linkage at all: name is local */
file_scope_calls++;
return ++local_counter;
}
int main(void)
{
printf("id %d\n", next_id());
printf("id %d\n", next_id());
printf("id %d\n", next_id());
printf("calls: %d\n", file_scope_calls);
return 0;
}
Example explained
Line 1local_counter keeps its value between calls, but its identifier has no linkage, so nothing outside next_id can name it, not even code in the same file.
Line 2file_scope_calls has internal linkage: every function in this file can name it, no other file can.
Line 3Both objects have static storage duration, which shows that the keyword's effect on lifetime is independent of its effect on linkage.
Line 4Removing static from local_counter changes lifetime and resets it each call; removing static from file_scope_calls changes only visibility to the linker.
Important notes
Order matters within a translation unit: static after a non-static declaration of the same name is rejected (gcc: static declaration follows non-static declaration), while extern after static is legal and keeps internal linkage.
In C, a file-scope const object still has external linkage by default, so const int MAX = 10; in a shared header collides at link time; add static or move the definition into one .c file.
Common mistakes
Writing int config; in a header instead of extern int config;. Every including file then defines it, and modern gcc (which defaults to -fno-common) fails the link with a multiple definition error.
Declaring extern int count; in a header while the defining file writes static int count;. The static definition has internal linkage and cannot satisfy the external reference, so the link fails with undefined reference to count even though both files compile.
Marking a helper static and later calling it from a second file after copying its prototype into a header. Compilation of the caller succeeds because the prototype looks fine, and the failure only appears at link time.
Try it yourself
Change, predict, then run
In one file, write extern int budget; at file scope and print budget from main with no definition anywhere, then compile and read the undefined reference error the linker reports. Add int budget = 100; at the bottom of the same file and confirm it now links and prints 100, which proves the extern line reserved no storage.
Open the C workspaceCheck your understanding
a.c contains static int counter = 0; and a non-static int bump(void) { return ++counter; }. b.c contains its own static int counter = 100; calls bump() twice, then prints its counter. What happens?
- The link fails, because counter is defined in two object files
- It prints 100, because each file has its own counter and bump() only touches a.c's
- It prints 102, because bump() increments whichever counter is in scope at the call site
- It prints 2, because the two definitions are merged and the initializer in b.c is discarded
Show answer
static gives each file-scope counter internal linkage, so the two objects are unrelated: the linker never sees either name, there is nothing to collide, and bump() was compiled against a.c's object only. The multiple definition answer is tempting because it is exactly what would happen if both files dropped static, since two external definitions of the same name cannot be merged under gcc's default -fno-common.