Skip to content
VLSI Mentor

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

ScopeWhere a declaration gets itVisible
blockinside { }, including function parametersfrom the declaration to the closing brace
fileoutside every functionfrom the declaration to the end of the translation unit
functionlabels only (Chapter 4.8's goto targets)anywhere in the function
function prototypeparameter names in a declaration, not a definitionto 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 durationBeginsEndsFrom
automaticwhen its block is enteredwhen that block exitsan ordinary local
staticbefore mainwhen the program endsfile scope, or static in a block
allocatedat mallocat freeModule 9
threadat thread startat 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

LinkageMeaningDefault for
externalone entity for the whole programfile-scope functions and objects
internalone entity per translation unitanything at file scope with static
nonethis declaration is its own entityblock-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

A table of four declarations against the three properties each one carries. int x at file scope has file scope, static lifetime and external linkage. static int x at file scope has file scope, static lifetime and internal linkage — the static keyword changed the linkage column, highlighted. int x inside a block has block scope, automatic lifetime and no linkage. static int x inside a block has block scope, static lifetime and no linkage — here the same keyword changed the lifetime column instead, highlighted. The two highlighted cells are in different columns, which is why static appears to mean two things.The declarationScopeLifetimeLinkageint x;at file scopefilestaticexternalstatic int x;at file scopefilestaticinternalint x;inside a blockblockautomaticnonestatic int x;inside a blockblockstaticnone12
Figure 1 — four declarations against the three properties each one carries. Read the two highlighted cells together, because they are the whole point: in the second row the static keyword changed the linkage column and left lifetime alone; in the fourth row the identical keyword changed the lifetime column and left linkage alone. Nothing about the word differs between the two rows. What differs is which property was available to change — a file-scope object already had static lifetime, so static could only affect its linkage, and a block-scope object already had no linkage, so static could only affect its lifetime.

Here is one program exhibiting every row, with the properties annotated:

Azvya Education Pvt. Ltd.VLSI Mentor
all three axes in one file
#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;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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=99

Read 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

ScopeLifetimeLinkageHow to write it
filestaticexternalint x; or int x = 1; at file scope
filestaticinternalstatic int x; at file scope
blockautomaticnoneint x; in a block
blockstaticnonestatic int x; in a block
blockstaticexternalextern int x; in a block — a declaration only
blockallocatednoneint *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?

SpecifierAt file scopeAt block scope
(none)——
staticlinkage → internallifetime → static
externmakes it a declaration, keeps external linkagemakes it a declaration with external linkage
registernot alloweda 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. static at file scope changes linkage. → file scope, static lifetime, internal linkage.
  • static int count = 0; inside a function. Block. Defaults: automatic lifetime, no linkage. static at block scope changes lifetime. → block scope, static lifetime, no linkage.
  • extern int count; at file scope. File. extern makes 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

  1. A declaration answers three independent questions: scope (where the name is visible), storage duration (how long the object lives), linkage (one entity or two).
  2. They belong to different stages: scope is compile-time, lifetime is run-time, linkage is link-time.
  3. Scope: block, file, function (labels), function-prototype.
  4. Duration: automatic, static, allocated, and thread from C11.
  5. Linkage: external, internal, none.
  6. Measured: automatic was 3 on every call while persists counted 1, 2, 3 — same scope, same linkage, different lifetime.
  7. Measured: b _demo.persists — static lifetime and no linkage, in one symbol, with its function's name attached.
  8. static changes linkage at file scope and lifetime at block scope — Figure 1's two highlighted cells, in different columns.
  9. The reason: at file scope lifetime is not open to change; at block scope linkage is not. The keyword changes whichever property is available.
  10. Not expressible: automatic lifetime with external linkage, a block-scope definition with external linkage, file scope with automatic lifetime.
  11. The procedure: where is it · what are the defaults there · what does the specifier change.
  12. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 — automatic 3, 3, 3 against persists 1, 2, 3.
  • Measured: b _demo.persists — static lifetime with no linkage, named after its function.
  • static changes 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.