C Part I · Module 11 · Building Multi-File C Programs
Scope, Lifetime and Linkage: Three Different Questions
You have met all three of these properties already, each in its own module and each on its own terms — where a name is visible came with functions, how long an object lives came with memory, and whether two files mean one entity came one chapter ago. What has never been said is that they are three independent questions, answered separately, by one declaration. That independence is the reason a single keyword has seemed to mean two unrelated things since Module 9: static does not have two meanings so much as one meaning applied to two different axes, and which axis depends entirely on where you wrote it. This chapter lays the three out together, shows which combinations C can actually express and which it cannot, and leaves you with a three-question procedure that reads any declaration without recognition or guesswork.
Advanced21 min readCPart IFundamentalsBuild
Part I · Module 11 · Chapter 11.5 · Scope, Lifetime and Linkage: Three Different Questions
1. Three Questions, Not One
Every declaration of an object in C answers three questions. They are independent, they have different answer sets, and nothing requires them to move together.
Scope — where in the source is this name visible? Block · file · function · function-prototype.
Storage duration (lifetime) — how long does the object exist? Automatic · static · allocated · (thread, added in C11).
Linkage — do declarations of this name in different places mean one entity? External · internal · none.
Each has already been taught: scope in Chapter 5.6, lifetime in Chapters 9.1 to 9.3, linkage in Chapter 11.4. What this chapter adds is that they are axes, and that confusion about static is entirely a confusion about which axis it is moving.
Notice immediately that they ask about different things. Scope is about the name and exists only while compiling. Lifetime is about the object and exists only while running. Linkage is about identity between declarations and is settled by the linker. Three properties, three different stages of the program's life.
2. Scope: Where the Name Is Visible
| Scope | Where a declaration gets it | Visible |
|---|---|---|
| block | inside { }, including function parameters | from the declaration to the closing brace |
| file | outside every function | from the declaration to the end of the translation unit |
| function | labels only (Chapter 4.8's goto targets) | anywhere in the function |
| function prototype | parameter names in a declaration, not a definition | to the end of the prototype |
Two things worth re-stating because they catch people out.
File scope starts at the declaration, not at the top of the file. A function cannot call something declared below it unless a declaration appears above — which is why headers go at the top.
Block scope nests and shadows. An inner declaration of the same name hides the outer one for the rest of the block, which Chapter 5.6 covered and -Wshadow reports.
Scope is a compile-time property. By the time the program runs, names are gone.
3. Lifetime: How Long the Object Exists
| Storage duration | Begins | Ends | From |
|---|---|---|---|
| automatic | when its block is entered | when that block exits | an ordinary local |
| static | before main | when the program ends | file scope, or static in a block |
| allocated | at malloc | at free | Module 9 |
| thread | at thread start | at thread end | _Thread_local, C11 — out of scope for Part I |
Lifetime is a run-time property, and Chapter 9.1 established the rule that matters: using a pointer to an object whose lifetime has ended is undefined, whichever duration it had.
4. Linkage: One Entity or Two
| Linkage | Meaning | Default for |
|---|---|---|
| external | one entity for the whole program | file-scope functions and objects |
| internal | one entity per translation unit | anything at file scope with static |
| none | this declaration is its own entity | block-scope objects, parameters, typedefs, tags |
Linkage is a link-time property, and Chapter 11.4 measured it in the symbol table: uppercase external, lowercase internal, absent for no linkage — with one exception that §5 is about.
5. The Axes Are Independent
Here is one program exhibiting every row, with the properties annotated:
#include <stdio.h>
int external_object = 1; /* file · static · external */
static int internal_object = 2; /* file · static · internal */
void demo(void); /* file · --- · external */
static void helper(void); /* file · --- · internal */
static void helper(void) { }
void demo(void)
{
int automatic = 3; /* block · automatic · none */
static int persists = 0; /* block · static · none */
extern int external_object; /* block · static · external */
persists++;
printf(" automatic=%d persists=%d external_object=%d internal_object=%d\n",
automatic, persists, external_object, internal_object);
helper();
}
int main(void)
{
demo();
demo();
demo();
{
int automatic = 99;
printf("inner block automatic=%d\n", automatic);
}
return 0;
} automatic=3 persists=1 external_object=1 internal_object=2
automatic=3 persists=2 external_object=1 internal_object=2
automatic=3 persists=3 external_object=1 internal_object=2
inner block automatic=99Read the first two columns across the three calls.
automatic is 3 every time. A new object on each entry to demo, initialised afresh — automatic lifetime.
persists counts 1, 2, 3. One object, surviving between calls — static lifetime.
They have the same scope (both block) and the same linkage (both none). Only the lifetime differs. That is the independence, in one output block.
And the symbol table separates them again from the other side:
T _demo file scope · external
b _demo.persists block scope · static lifetime · no linkage
D _external_object file scope · static · external
t _helper file scope · internal
d _internal_object file scope · static · internal
T _main_demo.persists is the row that settles it. It is in the symbol table — it has static lifetime, so the linker had to allocate and name it. And it is lowercase and compound — no other translation unit can reach it, and neither can another function in this one, because its name carries demo. Static lifetime with no linkage at all, which would be a contradiction if the two were one property.
The third local, extern int external_object;, is the fourth combination: block scope, static lifetime, external linkage. It produces no new symbol because it is a declaration, not a definition.
6. What C Can and Cannot Express
Not every point in the three-dimensional space is reachable, and knowing which are missing stops you looking for a keyword that does not exist.
| Scope | Lifetime | Linkage | How to write it |
|---|---|---|---|
| file | static | external | int x; or int x = 1; at file scope |
| file | static | internal | static int x; at file scope |
| block | automatic | none | int x; in a block |
| block | static | none | static int x; in a block |
| block | static | external | extern int x; in a block — a declaration only |
| block | allocated | none | int *p = malloc(...); — the pointer is automatic |
And the combinations that are not expressible:
An automatic object with external linkage. Nothing can share an object that ceases to exist when a block exits, so C does not offer it. This is Chapter 9.2's returned-local problem stated as a language rule rather than a bug.
A block-scope definition with external linkage. extern inside a block declares; it cannot define. The definition must be at file scope.
File scope with automatic lifetime. There is no block to end it, so the question does not arise.
A name visible in another file but not in this one. Linkage is not an access-control mechanism — static hides a name from other translation units, never from its own.
Read the gaps as constraints rather than omissions. Each one is C refusing to let you ask for something incoherent, and each has a workaround that is actually a different design: share an object by giving it static lifetime, or share it by giving one owner and passing a pointer (Chapter 9.8).
7. Reading Any Declaration in Three Steps
This is the procedure to take away. It works on every declaration and needs no recognition.
1. Where is it? File scope or block scope. This alone fixes the scope answer and determines what the storage-class specifier is allowed to change.
2. What is the default for that position? File scope: static lifetime, external linkage. Block scope: automatic lifetime, no linkage.
3. What does the specifier change?
Specifier At file scope At block scope (none) — — staticlinkage → internal lifetime → static externmakes it a declaration, keeps external linkage makes it a declaration with external linkage registernot allowed a hint; also forbids &(Chapter 9.12 §2)
Worked on four examples:
static int count = 0;at file scope. Block or file? File. Defaults: static lifetime, external linkage.staticat file scope changes linkage. → file scope, static lifetime, internal linkage.static int count = 0;inside a function. Block. Defaults: automatic lifetime, no linkage.staticat block scope changes lifetime. → block scope, static lifetime, no linkage.extern int count;at file scope. File.externmakes it a declaration. → file scope, static lifetime, external linkage, defines nothing.int count;as a parameter. Block (function-prototype scope in a declaration). Defaults apply, no specifier. → block scope, automatic lifetime, no linkage.
8. Why This Matters Beyond Vocabulary
Four common bugs are each a confusion of exactly two of these axes.
"I made it static so it would be shared." Linkage mistaken for lifetime. Chapter 11.3 §5 measured the result: a static object in a header gave two files two independent copies — "a sees 2, b sees 1".
"The local is still there after the function returns." Lifetime mistaken for scope. The name went out of scope and the object's lifetime ended; Chapter 9.2's returned local is the bug, and a static local is the fix precisely because it changes lifetime without changing scope.
"Two files both declare it, so they share it." Linkage assumed rather than arranged. Two int x; definitions are Chapter 11.1 §5's tentative-definition trap; extern in a header plus one definition is the arrangement.
"It compiles here so the name must be private." Scope mistaken for linkage. A name not declared in another file's header is still externally linked and still collides — which is why Chapter 11.4's rule is to write static, not to rely on nobody knowing.
In each case the reader had one property and reasoned as though they had another. Three questions, asked separately, prevent all four.
9. What You Should Be Able to Say Now
- A declaration answers three independent questions: scope (where the name is visible), storage duration (how long the object lives), linkage (one entity or two).
- They belong to different stages: scope is compile-time, lifetime is run-time, linkage is link-time.
- Scope: block, file, function (labels), function-prototype.
- Duration: automatic, static, allocated, and thread from C11.
- Linkage: external, internal, none.
- Measured:
automaticwas 3 on every call whilepersistscounted 1, 2, 3 — same scope, same linkage, different lifetime. - Measured:
b _demo.persists— static lifetime and no linkage, in one symbol, with its function's name attached. staticchanges linkage at file scope and lifetime at block scope — Figure 1's two highlighted cells, in different columns.- The reason: at file scope lifetime is not open to change; at block scope linkage is not. The keyword changes whichever property is available.
- Not expressible: automatic lifetime with external linkage, a block-scope definition with external linkage, file scope with automatic lifetime.
- The procedure: where is it · what are the defaults there · what does the specifier change.
- Four common bugs are each two axes confused — shared-via-
static, the surviving local, two definitions assumed to be one, and privacy assumed from obscurity.
10. Exercises
Exercise 1 — fill the three columns. For each declaration give scope, storage duration and linkage.
int a = 1; /* file scope */
static int b; /* file scope */
void f(int c) { int d; static int e; extern int a; }
static void g(void) { }
const char *h = "x"; /* file scope */Exercise 2 — use the procedure. Apply §7's three steps, writing each step out, to static unsigned long tick_count; appearing (a) at file scope and (b) inside tick(). Say what differs and what does not.
Exercise 3 — name the impossible. For each, say whether C can express it and, if not, what the closest expressible design is.
- an object visible in two files that is destroyed when a function returns
- an object visible only inside one function that survives between calls
- an object visible in one file only, living for the whole program
- a definition inside a block that other files can link to
Exercise 4 — explain the symbol. nm shows b _demo.persists and d _internal_object. Give all three properties for each, and explain why only one of the two needed a compound name.
Exercise 5 — diagnose the bug. A header contains static int connection_count; and four .c files include it. Using the three axes, say which property the author intended to set, which they actually set, what the program does, and what the fix is.
Exercise 6 — the returned local. Explain, in terms of two axes, why returning &local is a bug and why adding static to local removes it. Say precisely which property changed and which did not.
Exercise 7 — argue the keyword. In four sentences, argue that static has one meaning rather than two, using the §8 callout's reasoning. Then give the one sentence you would tell a beginner instead.
Exercise 8 — audit a file. A .c file has: three file-scope objects with no keyword, two file-scope functions with no keyword, one static local counter, and a header declaring one of the objects. For each item give the three properties and say whether it should change.
11. Summary
- A declaration answers three independent questions: scope, storage duration, linkage — compile-time, run-time, link-time.
- Scope: block · file · function · prototype. Duration: automatic · static · allocated · thread. Linkage: external · internal · none.
- Measured: same scope and linkage, different lifetime —
automatic3, 3, 3 againstpersists1, 2, 3. - Measured:
b _demo.persists— static lifetime with no linkage, named after its function. staticchanges linkage at file scope and lifetime at block scope — because in each position only one of the two is open to change.- Not expressible: automatic + external, a block-scope definition with external linkage, file scope with automatic lifetime.
- The procedure: where is it · what are the defaults · what does the specifier change.
- Four classic bugs are two axes confused.
Everything in this module so far has been about what the compiler and linker do with your declarations. One stage runs before either of them and obeys none of their rules: the preprocessor, which Chapter 11.2 measured turning seven lines into 573 and which has been quietly present in every include guard since. It does not know about scope, it does not know about types, and it certainly does not know about linkage — it replaces tokens with other tokens and hands the result on. That is enough to be genuinely useful and enough to be dangerous in ways that look nothing like ordinary C bugs: an expression that computes the wrong thing because an operator bound differently than you expected, and an argument that gets evaluated twice because the text mentioning it appeared twice. The next chapter is about writing macros that survive substitution, and about the two traps that have caught every C programmer at least once.
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.
