Skip to content
VLSI Mentor

C Part I · Module 11 · Building Multi-File C Programs

Linkage: extern, static and Names Shared Across Files

Three chapters have relied on a word without explaining it. A header declares extern int level; and one source file defines int level = 1;, and somehow those two names are the same object — while a static int in another file is a different object even if it has the same name. The property doing that work is called linkage, and it answers exactly one question: when a name appears in two translation units, does it mean one entity or two? C answers with three possibilities and two keywords, and the keywords are the confusing part, because static spells one of those answers at file scope and spells something completely unrelated inside a function. This chapter takes the property first and the keywords second, and makes both visible in the one place where they stop being abstract: the symbol table of a compiled object file.

Intermediate20 min readCPart IFundamentalsBuild

Part I · Module 11 · Chapter 11.4 · Linkage: extern, static and Names Shared Across Files

1. The Question Linkage Answers

Linkage decides whether a name appearing in more than one place refers to the same entity. C has three answers:

  • External linkage — all declarations of the name, anywhere in the program, refer to one entity.
  • Internal linkage — all declarations of the name within one translation unit refer to one entity, and other units get their own.
  • No linkage — the declaration is its own entity, referring to nothing outside itself.

Note what the question is not. It is not "where can I write this name" — that is scope, and Chapter 5.6 covered it. It is not "how long does the object live" — that is lifetime, and Chapter 9.1 covered that. Linkage is specifically about identity across declarations, and Chapter 11.5 is where all three are finally laid out together.

2. The Defaults, Before Any Keyword

Most declarations have linkage without you asking for it, and knowing the defaults is more useful than knowing the keywords.

DeclarationLinkageMeaning
a function at file scopeexternalcallable from any file that declares it
an object at file scopeexternalone object for the whole program
a block-scope object (an ordinary local)noneits own thing, every time
a function parameternone—
a typedef, a struct tag, an enumeration constantnonenot entities the linker knows about

So int level = 1; at file scope is already shared with the whole program, with no keyword at all. The keywords exist to change that, not to enable it.

3. Making It Visible

Linkage is not an abstraction you have to take on faith; it is written in the object file.

Azvya Education Pvt. Ltd.VLSI Mentor
shared.h
#ifndef SHARED_H
#define SHARED_H
extern int  level;          /* DECLARATION: one object, defined elsewhere */
void        set_level(int);
int         get_level(void);
#endif
Azvya Education Pvt. Ltd.VLSI Mentor
shared.c
#include "shared.h"

int level = 1;                      /* the ONE definition: external linkage  */

static int change_count = 0;        /* internal linkage: private to this file */
static void note(void) { change_count++; }

void set_level(int n) { level = n; note(); }
int  get_level(void)  { return level; }
int  changes(void)    { return change_count; }
Azvya Education Pvt. Ltd.VLSI Mentor
user.c
#include <stdio.h>
#include "shared.h"
int changes(void);

int main(void)
{
    printf("start level=%d\n", level);          /* the SAME object */
    level = 5;
    printf("after direct write=%d\n", get_level());
    set_level(9);
    printf("after set_level=%d changes=%d\n", get_level(), changes());
    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
start level=1
after direct write=5
after set_level=9 changes=1

user.c wrote level directly and shared.c's get_level read the change. One object, reached from two translation units.

Now the symbol table of shared.o:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ nm shared.o
0000000000000058 D _level           <- external, initialised data
0000000000000000 T _set_level       <- external, code
0000000000000040 T _get_level       <- external, code
000000000000004c T _changes         <- external, code
00000000000000e0 b _change_count    <- INTERNAL (lowercase)
000000000000002c t _note            <- INTERNAL (lowercase)

Uppercase means external linkage: the symbol is offered to the linker, and any other object file may claim it. Lowercase means internal: the symbol exists for this object file's own use and the linker will not match anything else to it.

That is linkage, in one column of letters.

4. static at File Scope: Internal Linkage

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static int change_count = 0;
    static void note(void) { ... }

At file scope, static means internal linkage: this name belongs to this translation unit. Another file may declare its own change_count or note with no collision, and neither can reach the other's.

What that buys you is worth stating precisely, because "hiding" sounds like a style preference.

It makes the file's interface explicit. Everything not static is part of what other files may use, and a reader can tell which is which from the keyword alone.

It prevents collisions. Chapter 11.2 §5 measured two files each with static int helper coexisting. Without static that is a duplicate symbol at link time.

It lets the compiler do more. A static function has no callers outside the translation unit by construction, so the compiler can inline it freely, prove things about its arguments, or delete it entirely if it is unused — none of which it can do for a function the whole program might call.

And it turns future mistakes into errors. If nobody outside may call note, then an attempt to do so fails at build time instead of quietly creating a dependency you did not intend.

Default to static at file scope. Make a name external when another file genuinely needs it, and say so by putting a declaration in the header — which is the only place anyone should learn that it is available.

5. extern: Declaring Without Defining

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    extern int level;            /* a declaration: there is one, elsewhere */

At file scope, extern on an object says "this name has external linkage and this declaration is not the definition." It creates nothing — Chapter 11.1 §2 measured a declarations-only object file at zero bytes — and it is exactly what belongs in a header.

Three details.

On a function declaration, extern is redundant. int add(int, int); already has external linkage by default (§2). Writing extern int add(int, int); is legal and adds nothing; most codebases omit it.

Without extern, a file-scope object declaration is a definition. int level; in a header is a tentative definition in every including file — Chapter 11.1 §5's portability trap, which is why the header says extern.

With an initialiser, extern is a definition, which is the one case where the keyword does not mean what it usually means:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    extern int z = 3;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: 'extern' variable has an initializer [-Wextern-initializer]

It compiles, it defines z, and it prints 3. The warning is telling you the extern is doing nothing. Do not write it; recognise it.

extern also works at block scope, where it declares a name with external linkage inside a function:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    static void show(void)
    {
        extern int shared_total;      /* a declaration, not a local object */
        printf("%d\n", shared_total);
    }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
shared_total seen from inside the block: 7

That reads the file-scope object, not a new local. It is legal and almost always the wrong style — the declaration is invisible to anyone reading the top of the file, and a header does the job better. Know it so you can read it.

6. The Two Meanings of static

static spells two unrelated properties depending on where it appears, and C reused the keyword because the two situations never overlap.

Azvya Education Pvt. Ltd.VLSI Mentor
both in one file
#include <stdio.h>

static int file_scope_static = 100;        /* meaning 1: INTERNAL LINKAGE */

static int counter(void)
{
    static int calls = 0;                  /* meaning 2: STATIC LIFETIME  */
    calls++;
    return calls;
}

int main(void)
{
    printf("counter: %d %d %d\n", counter(), counter(), counter());
    printf("file_scope_static = %d\n", file_scope_static);
    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
counter: 1 2 3
file_scope_static = 100

At file scope, static controls linkage — it makes the name private to the translation unit. The object was already going to live for the whole program (Chapter 9.3's static storage duration); static changed only who can see it.

At block scope, static controls lifetime — it makes the object live for the whole program instead of for one call (Chapter 9.3 again). The object had no linkage before and still has none; static changed only how long it lives.

And the symbol table shows they are genuinely different mechanisms:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
00000000000000cc d _file_scope_static
0000000000000110 b _counter.calls

Both lowercase, both private to the file — and the second one carries its function's name. _counter.calls is the object belonging to calls inside counter; the linker has to invent a unique name for it because a second function could have its own calls. A file-scope static needs no such treatment because its name is already unique in the file.

Two keywords' worth of meaning in one word. At file scope: who can see this name. Inside a function: how long this object lives. They are not variations on one idea, and Chapter 11.5 is where the confusion is finally dismantled by separating all three properties.

7. The Same Mistake, Caught at Two Distances

Here is the measurement that justifies Chapter 11.3's header discipline more directly than anything in that chapter.

Take the working program from §3 and make get_level static by mistake — forgetting that user.c calls it.

With the header included in shared.c:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
shared_static.c:6:12: error: static declaration of 'get_level' follows
                     non-static declaration
    6 | static int get_level(void) { return level; }
      |            ^
./shared.h:5:13: note: previous declaration is here
    5 | int         get_level(void);
      |             ^

A compile error, in the file containing the mistake, naming the line and the contradicting declaration. The header put the declaration and the definition in one translation unit, so the compiler could compare them — Chapter 11.2's boundary working for you.

Now the same mistake where no header exists and the caller declares the function itself:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Undefined symbols for architecture arm64:
  "_hidden_fn", referenced from:
      _main in h2-5c5c84.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1

A link error, after both files compiled cleanly, naming the symbol and the file that wanted it but not the file that should have provided it — because by then nothing remembers that it ever existed.

One defect, two distances. With a header: caught while compiling the file you got wrong, with both lines quoted. Without: caught at the end of the build, from the other side, with no mention of the cause. The header is not saving you typing; it is moving the detection closer to the mistake.

8. The Policy

Everything in this chapter reduces to two rules, and they are the ones c.ts asks this chapter to establish.

Exactly one definition for everything shared. One .c file defines it; the header declares it with extern (objects) or an ordinary prototype (functions); every file that uses it — including the one that defines it — includes that header.

Internal linkage for everything private. If no other file needs it, write static. The default is external, so privacy is something you have to ask for.

In practice:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    /* thing.h */                        /* thing.c */
    #ifndef MYPROJ_THING_H               #include "thing.h"
    #define MYPROJ_THING_H
    extern int thing_count;              int thing_count = 0;
    void thing_reset(void);              static int internal_state = 0;
    #endif                               static void helper(void) { ... }
                                         void thing_reset(void) { helper(); }

Four properties follow, and all four were measured in this chapter: thing_count is one object; thing_reset is callable; internal_state and helper are unreachable from anywhere else and will not collide with any other file's; and any mistake about which is which becomes a compile error in thing.c rather than a link error somewhere else.

One loose end from Chapter 11.3 §5 can now be tied off, because it is a linkage question. A plain inline function definition in a header is not safe — inline alone does not settle which translation unit provides the out-of-line copy, and C's rules for that are intricate. static inline is safe, and §4 already explains why: static gives the function internal linkage, so each including translation unit gets its own private copy, no two copies are ever offered to the linker, and the duplicate-symbol failure of Chapter 11.3 §3 cannot arise. The cost is the one §4 described for any static in a header — one compiled copy per including file, and a different address in each — which is acceptable for a small function and is exactly what you are asking for when you write inline.

9. What You Should Be Able to Say Now

  1. Linkage decides whether a name in two places means one entity or two. Three answers: external, internal, none.
  2. Defaults: file-scope functions and objects are external; block-scope objects, parameters, typedefs and tags have none.
  3. So a file-scope int level = 1; is already shared with the whole program — the keywords change that, they do not enable it.
  4. nm shows it: uppercase = external, lowercase = internal. Measured: D _level, T _set_level against b _change_count, t _note.
  5. Measured: user.c wrote level and shared.c read the change — one object, two translation units.
  6. static at file scope = internal linkage. It makes the interface explicit, prevents collisions, lets the compiler optimise more, and turns future misuse into a build error.
  7. Default to static; make a name external deliberately, via the header.
  8. extern at file scope declares without defining — redundant on functions, essential on objects in a header.
  9. extern with an initialiser is a definition — "'extern' variable has an initializer", and it still defines the object.
  10. static means two different things: linkage at file scope, lifetime inside a function. The symbol table proves it — _counter.calls carries its function's name.
  11. Measured: the same mistake was a compile error with a header ("static declaration follows non-static declaration") and an undefined-symbol link error without one.
  12. One definition for everything shared; internal linkage for everything private.

10. Exercises

Exercise 1 — give the linkage. For each, state external, internal, or none.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int total;
static int total;
void run(void);
static void run(void);
int main(void) { int n; static int m; extern int q; }
typedef int metres;
struct frame { int id; };

Exercise 2 — predict the symbol letters. What would nm show for each, and which are offered to the linker?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int a = 1;
static int b = 2;
int c;
static void d(void) { }
void e(void) { static int f = 0; (void)f; }

Exercise 3 — place the declarations. A project shares an object log_level, a function log_write, and keeps a private buffer scratch and a private helper flush. Write log.h and the top of log.c.

Exercise 4 — two distances. A function is made static by mistake and another file calls it. Give both error messages from §7, say which file each names, and explain in one sentence why the header version is better.

Exercise 5 — the extern exception. Explain what extern int z = 3; does, what the compiler says, and why the warning exists rather than an error.

Exercise 6 — the two statics. For each, say which property static controls and what would change if it were removed.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static int table[64];                       /* at file scope       */
void f(void) { static int calls = 0; }      /* inside a function   */
static void g(void) { }                     /* at file scope       */

Exercise 7 — explain the symbol name. nm shows b _counter.calls. Say what declaration produced it, why the linker needed a compound name, and what a second function with its own calls would produce.

Exercise 8 — audit for privacy. Given a .c file with eleven file-scope functions of which three are declared in the header, say what should change and what four benefits §4 says you would gain.

11. Summary

  • Linkage = does this name mean one entity or two? External · internal · none.
  • Defaults: file scope is external; block scope and parameters have none.
  • nm makes it visible — uppercase external, lowercase internal. Measured: D _level, T _set_level, b _change_count, t _note.
  • Measured: two files reached one object — write in one, read the change in the other.
  • static at file scope = internal linkage — explicit interface, no collisions, better optimisation, earlier errors.
  • Default to static. External is the default, so privacy must be asked for.
  • extern declares without defining — redundant on functions, required on header objects; with an initialiser it defines, with a warning.
  • static means two unrelated things; _counter.calls in the symbol table proves it.
  • Measured: the same defect was a compile error with a header and an undefined-symbol link error without.
  • One definition for everything shared; internal linkage for everything private.

The previous section used three words in close succession and treated them as obviously distinct — a block-scope object has no linkage, lives for one call, and is visible within its block. Those are three independent answers to three independent questions, and nothing forces them to move together: an object can be visible in one function and live for the whole program, or be visible in every file and live only while a block runs. C has a single keyword that changes one of the three in one context and a different one in another, which is why the same word has felt like two words throughout this chapter. The next chapter puts all three properties in one table, works out which combinations C can actually express, and settles the question that has been accumulating since Module 5 — which is not what static means, but why the question has two answers at all.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.