C Part I · Module 11 · Building Multi-File C Programs
Declarations vs Definitions
Every program you have written so far has been one file, and in one file the difference between telling the compiler that a name exists and actually creating the thing it names almost never comes up — you write int count = 0; and both happen at once. A real project is many files, and there the two acts separate completely: one file creates the object and the others are merely told about it. Get the distinction wrong and you meet an error message about duplicate symbols, or a variable that mysteriously has two independent values, or — worst of all — a program that builds without complaint and is wrong, because the thing one file was told about is not the thing another file created. This chapter establishes the distinction, measures it in bytes, and then shows the gap in the toolchain that the rest of the module exists to close.
Intermediate19 min readCPart IFundamentalsBuild
Part I · Module 11 · Chapter 11.1 · Declarations vs Definitions
1. The Distinction
A declaration introduces a name and says what type it has. A definition is a declaration that also creates the thing — storage for an object, a body for a function, members for a type.
Every definition is a declaration. Not every declaration is a definition.
extern int count; /* declaration: there is an int called count, somewhere */
int count; /* definition: here is one */
int count = 0; /* definition: here is one, set to 0 */
int add(int, int); /* declaration: there is a function called add */
int add(int a, int b) /* definition: here it is */
{ return a + b; }
struct node; /* declaration: there is a type called struct node */
struct node { int v; }; /* definition: and these are its members */The test is simple: ask whether the compiler now has to produce something. A declaration lets it check the name's use; a definition is the thing itself.
2. Measuring the Difference
This is not an abstraction. Compile a file containing only declarations:
/* DECLARATIONS ONLY */
extern int shared_counter; /* declaration of an object */
int add(int, int); /* declaration of a function */
struct node; /* declaration of a type (incomplete) */
typedef unsigned long ticks; /* a name, but no object */$ cc -std=c17 -Wall -Wextra -Wpedantic -c a_decl.c -o a_decl.o
$ size a_decl.o
__TEXT __DATA __OBJC others dec hex
0 0 0 0 0 0Zero. No code, no data, nothing. The object file is a valid, complete translation unit that contributes not one byte to any program.
Now a file of definitions:
int shared_counter = 7; /* object, initialised */
int uninitialised_global; /* tentative definition */
static int private_total = 0; /* object, file-local */
struct node { int v; struct node *next; }; /* completes the type */
int add(int a, int b) { return a + b; } /* function, with a body */
int bump(void) { private_total++; return private_total; }$ nm b_def.o
0000000000000000 T _add
0000000000000020 T _bump
0000000000000080 b _private_total
0000000000000038 D _shared_counter
0000000000000004 C _uninitialised_globalFive entries, and the letters are informative:
| Letter | Meaning |
|---|---|
T | code, visible to other translation units |
D | initialised data, visible to other translation units |
C | a common symbol — §5 |
lowercase (b, t, d) | the same thing, but not visible outside this file |
private_total appears in lowercase because of the static keyword, which is Chapter 11.4's subject. Note also what is absent: struct node and the typedef produce no symbol at all. Types are not things a linker knows about — they exist only while compiling.
A declaration costs nothing and can appear anywhere, any number of times. A definition creates something and has to appear exactly once. The rest of this module is the consequences of that sentence.
3. Declarations May Repeat; Definitions May Not
int add(int, int);
int add(int, int);
int add(int, int); /* all fine */
int add(int a, int b) { return a + b; }$ cc -std=c17 -Wall -Wextra -Wpedantic -c decls.c
$No complaint. Repeating a declaration says the same true thing again.
int add(int a, int b) { return a + b; }
int add(int a, int b) { return a + b; }error: redefinition of 'add'Even though the two bodies are identical. One definition, ever — because a definition creates something, and creating the same thing twice is not meaningful.
This is exactly why headers hold declarations. A header is included by many files, so everything in it must tolerate being repeated. Chapter 11.3 is about what that rule allows and forbids.
4. A Name With No Declaration
int main(void) { int r = add(2, 3); return r; } /* no declaration of add */C17: error: call to undeclared function 'add'; ISO C99 and later do not
support implicit function declarations
C23: error: use of undeclared identifier 'add'Both reject it, with different wording because they reject it for different reasons. C89 allowed this — an undeclared function was assumed to return int and take unchecked arguments, which is the "implicit function declaration" the C17 message names. C99 removed the rule, and C23 removed even the memory of it, treating add as simply undeclared.
If you meet old code that calls functions with no declaration in scope, that is why it used to compile. It is an error now, and the error is doing you a favour.
5. The Awkward Case: Tentative Definitions
int uninitialised_global; /* at file scope, with no initialiser */Is that a declaration or a definition? C's answer is "a third thing".
C17 §6.9.2: a file-scope declaration of an object with no initialiser and no storage-class specifier (or with
static) is a tentative definition. If the translation unit contains no ordinary definition of that identifier by the end, it behaves as though there were one, initialised to zero.
So within one file, this is fine and collapses to a single zero-initialised object:
int x;
int x;
int x; /* still one object, value 0 */Across files it becomes interesting, and the measurements are worth seeing because the answer is changing.
Two translation units, each containing int x;:
$ cc -std=c17 -Wall -Wextra -Wpedantic t1.c t2.c -o two
$ ./two
x=0It linked.
One tentative, one initialised:
$ cc -std=c17 -Wall -Wextra -Wpedantic t1.c t3.c -o two2 /* t3.c has int x = 5; */
$ ./two2
x=5That linked too, and the initialised one won.
The same two files, with -fno-common:
duplicate symbol '_x' in:
.../t1.o
.../t2.o
ld: 1 duplicate symbols
clang: error: linker command failed with exit code 1One more consequence worth knowing: static int x; at file scope is also a tentative definition, but with internal linkage, so each file gets its own private object and no merging question arises.
6. The Gap This Module Exists to Close
Here is the measurement that makes the rest of the module necessary.
Two files. The first declares add one way and calls it:
/* v1.c */
double add(double, double);
#include <stdio.h>
int main(void) { printf("%f\n", add(2, 3)); return 0; }The second defines it a completely different way:
/* v2.c */
int add(int a, int b) { return a + b; }Compile each on its own:
$ cc -std=c17 -Wall -Wextra -Wpedantic -c v1.c -o v1.o
$ cc -std=c17 -Wall -Wextra -Wpedantic -c v2.c -o v2.o
$Both clean. Neither file contains an error, because neither file can see the other.
Link them:
$ cc v1.o v2.o -o mismatch
$It links. A program now exists in which main calls a function expecting two doubles and arrives at a function expecting two ints. Running it is undefined behaviour, so no output from it appears in this chapter — the point is entirely that nothing stopped it being built.
Now put the same two declarations in one file:
double add(double, double);
int add(int, int);error: conflicting types for 'add'Instantly rejected.
The compiler is strict inside a translation unit and completely blind between them. The linker sees only names, not types. A symbol called
_addwas wanted and a symbol called_addwas supplied; that is the entire check performed.
That gap is the problem the whole module solves, and the solution is a single discipline: write each declaration once, in a header, and have both the file that defines the thing and every file that uses it include that header. Then the compiler — which does check — sees the declaration and the definition together, in one translation unit, and the mismatch above becomes error: conflicting types. Chapter 11.3 builds that discipline and Chapter 11.8 shows why it is the only point at which such an error can be caught.
7. Where Each One Belongs
| Kind | Declaration | Definition | Goes in |
|---|---|---|---|
| function | int add(int, int); | int add(int a, int b) { ... } | declaration → header · definition → one .c |
| object | extern int count; | int count = 0; | declaration → header · definition → one .c |
| struct/union/enum type | struct node; | struct node { ... }; | usually the definition goes in the header |
typedef | — | typedef unsigned long ticks; | header |
| macro | — | #define MAX 64 | header |
The third row is the one that surprises people and it follows directly from §2: a type definition creates no symbol, so repeating it in many translation units creates nothing to collide. What it must not do is appear twice in one translation unit, which is what Chapter 11.3's include guards prevent.
Rows four and five are the same point. A typedef creates a name, not an object; a macro is handled before the compiler proper sees anything. Neither reaches the linker.
8. What You Should Be Able to Say Now
- A declaration introduces a name and its type; a definition also creates the thing. Every definition is a declaration.
- Measured: a translation unit of pure declarations compiled to zero bytes of text and data, with no symbols.
- A translation unit of definitions produced symbols:
Tcode,Dinitialised data,Ccommon, lowercase for file-local. - Types and typedefs produce no symbols at all — the linker never hears about them.
- Declarations may repeat; definitions may not — "redefinition of 'add'", even with identical bodies.
- That is why headers hold declarations.
- Calling an undeclared function is an error in C99 and later; C89 allowed it via implicit declaration.
- A file-scope object with no initialiser is a tentative definition and behaves as a zero-initialised definition at the end of the translation unit.
- ISO C requires exactly one external definition (§6.9p5). Two files each with
int x;violates that; merging is the common extension (Annex J.5.11), and-fno-commonrejects it — measured. - Use
extern int x;in the header andint x = 0;in exactly one.c. - Measured: a function declared
double add(double,double)and definedint add(int,int)in two files compiled clean and linked. - The compiler is strict within a translation unit and blind between them; the linker matches names, not types. Closing that gap is what the rest of this module does.
9. Exercises
Exercise 1 — declaration or definition. Classify each, and say what it creates.
extern double rate;
double rate;
double rate = 1.5;
static double rate;
void reset(void);
void reset(void) { }
struct point;
struct point { int x; int y; };
typedef struct point Point;
#define SCALE 4Exercise 2 — predict the symbol letters. For each, say what nm would show — uppercase or lowercase, and which letter — or that nothing appears.
int total = 1;
static int hidden = 2;
int pending;
void go(void) { }
static void stop(void) { }
typedef int metres;Exercise 3 — count the definitions. How many objects does this translation unit create, and what are their values?
int a;
int a;
int a = 3;
int b;
static int c;Exercise 4 — the tentative trap. Two files each contain int config;. Say what happens on a toolchain that permits it, what happens with -fno-common, which is standard C, and give the two-line fix.
Exercise 5 — explain the gap. In three sentences, explain why §6's mismatched program built without complaint, naming what the compiler checks and what the linker checks.
Exercise 6 — close the gap. Rewrite §6's two files so the mismatch becomes a compile error. State which file must include what, and why including it in the defining file matters as much as in the calling one.
Exercise 7 — what belongs where. You have a project with config.c and three other .c files that all need a shared struct settings, a shared int verbose, a function load_settings, and a constant MAX_PATH. Say what goes in config.h and what stays in config.c.
Exercise 8 — the C89 question. You are handed code from 1992 that calls strlen with no #include <string.h>. Say why it compiled then, what your compiler says now, and what could have gone wrong at run time even when it did compile.
10. Summary
- A declaration names; a definition creates. Every definition is a declaration.
- Measured: declarations-only object, 0 bytes, no symbols; definitions produced
T,D,Cand lowercase local symbols. - Types and typedefs reach no linker.
- Declarations repeat freely; definitions must not — "redefinition", even with identical bodies. Hence headers hold declarations.
- Undeclared function calls are errors since C99.
- A file-scope object with no initialiser is a tentative definition; ISO C still demands exactly one external definition, and merging is the common extension that
-fno-commonrejects — measured all three ways. - The portable pattern:
extern int x;in the header,int x = 0;in one.c. - Measured: a type mismatch across two files compiled clean and linked.
- The compiler checks within a translation unit; the linker matches names only.
That last sentence used a term this chapter has leaned on without ever defining: translation unit. It is not the same thing as a file, and the difference is not pedantry — it is the reason a header you include is indistinguishable, by the time the compiler proper runs, from text you typed yourself. The next chapter pins the term down exactly: what the preprocessor hands to the compiler, how much of it comes from files you did not write, and why a single innocuous-looking #include <stdio.h> can turn a twelve-line program into tens of thousands of lines before a single token of it is compiled.
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.
