Skip to content
VLSI Mentor

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.

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

Azvya Education Pvt. Ltd.VLSI Mentor
a translation unit that creates nothing
/* 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      */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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	0

Zero. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
a translation unit that creates things
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; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ nm b_def.o
0000000000000000 T _add
0000000000000020 T _bump
0000000000000080 b _private_total
0000000000000038 D _shared_counter
0000000000000004 C _uninitialised_global

Five entries, and the letters are informative:

LetterMeaning
Tcode, visible to other translation units
Dinitialised data, visible to other translation units
Ca 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int add(int, int);
    int add(int, int);
    int add(int, int);           /* all fine */
    int add(int a, int b) { return a + b; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc -std=c17 -Wall -Wextra -Wpedantic -c decls.c
$

No complaint. Repeating a declaration says the same true thing again.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int add(int a, int b) { return a + b; }
    int add(int a, int b) { return a + b; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    int main(void) { int r = add(2, 3); return r; }      /* no declaration of add */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc -std=c17 -Wall -Wextra -Wpedantic t1.c t2.c -o two
$ ./two
x=0

It linked.

One tentative, one initialised:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc -std=c17 -Wall -Wextra -Wpedantic t1.c t3.c -o two2      /* t3.c has int x = 5; */
$ ./two2
x=5

That linked too, and the initialised one won.

The same two files, with -fno-common:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
duplicate symbol '_x' in:
    .../t1.o
    .../t2.o
ld: 1 duplicate symbols
clang: error: linker command failed with exit code 1

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    /* v2.c */
    int add(int a, int b) { return a + b; }

Compile each on its own:

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    double add(double, double);
    int    add(int, int);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 _add was wanted and a symbol called _add was 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

KindDeclarationDefinitionGoes in
functionint add(int, int);int add(int a, int b) { ... }declaration → header · definition → one .c
objectextern int count;int count = 0;declaration → header · definition → one .c
struct/union/enum typestruct node;struct node { ... };usually the definition goes in the header
typedef—typedef unsigned long ticks;header
macro—#define MAX 64header

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

  1. A declaration introduces a name and its type; a definition also creates the thing. Every definition is a declaration.
  2. Measured: a translation unit of pure declarations compiled to zero bytes of text and data, with no symbols.
  3. A translation unit of definitions produced symbols: T code, D initialised data, C common, lowercase for file-local.
  4. Types and typedefs produce no symbols at all — the linker never hears about them.
  5. Declarations may repeat; definitions may not — "redefinition of 'add'", even with identical bodies.
  6. That is why headers hold declarations.
  7. Calling an undeclared function is an error in C99 and later; C89 allowed it via implicit declaration.
  8. 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.
  9. 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-common rejects it — measured.
  10. Use extern int x; in the header and int x = 0; in exactly one .c.
  11. Measured: a function declared double add(double,double) and defined int add(int,int) in two files compiled clean and linked.
  12. 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.

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

Exercise 2 — predict the symbol letters. For each, say what nm would show — uppercase or lowercase, and which letter — or that nothing appears.

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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, C and 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-common rejects — 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.