---
title: 'Header Files'
source: 'https://academia.sh/en/courses/c-fundamentals/header-files'
course: 'C Fundamentals'
language: en
updated: '2026-09-26T03:02:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes the difference between a declaration and a definition through the source'
    - 'Shows by running it how a header file carries the same declaration into multiple units'
    - 'Explains with an example which error the include guard prevents'
---

# Header Files

A header file does not carry a name, it carries that name's text, and the preprocessor copies it separately into every unit that uses it. This makes it possible for the same declaration to enter two units at once and for both to compile; the measurement proves this by running it, and separately shows what the include guard prevents.

The previous lesson showed the linker class with only a single call: `identity`'s declaration
was in one file, its definition in another, and `stage.h` built the bridge between them. This
lesson looks at that bridge itself. A header file is neither a unit nor compiled on its own; it
only carries text, and that text is copied separately into every unit that `#include`s it.

This single sentence summarizes a fact that will be recalled often through the rest of the
course: in C, a unit is the union of its own source and everything it `#include`s. What the
compiler sees is not the form `stage.c` takes on screen, but the single piece of text the
preprocessor produces after it has resolved every `#include`. A header file determines where it
stands in this union, and this lesson shows why that position must be reserved for declarations
alone.

## Declaration and Definition

In C, a name has two different appearances in the source. A declaration says that a name
exists, its type, and how it is called, but gives no body. A definition gives what that name
corresponds to — a function's body, a variable's allocated place. A name can be declared in more
than one place, but can be defined in only one place; this distinction is the reason header
files exist.

A unit does not have to see the definition of every name it calls; seeing only its declaration
is enough — this is why a single header file can be shared among dozens of units. To produce a
call, the compiler must know the name's type, how many parameters it takes, and what it
returns — all of this is in the declaration. Where the name's actual counterpart is located is
a question that does not concern the compiler; the linker will answer that question. Because of
this distinction, a unit can call a definition it has never seen — as long as it has a correct
declaration.

```c
// stage.h — declarations of names defined in other units
#ifndef STAGE_H
#define STAGE_H

/* These two functions are defined in another unit: their calls are resolved at link time. */
int identity(void);
const char *unit_name(void);

/* This variable is also defined in another unit. */
extern int unit_counter;

#endif
```

`stage.h` declares three names, it defines none of them. `identity` and `unit_name` have no
body; the `extern` standing in front of `unit_counter` says exactly this — "this name is
defined elsewhere, it is only declared here." The definitions themselves stand in the second
unit.

```c
// choice.c — the second unit: definitions live here, calls resolve at link time
#include "stage.h"

int unit_counter = 2;

int identity(void) {
    return 7;
}

const char *unit_name(void) {
    return "choice";
}
```

The third file includes `stage.h` on its own, unaware of `choice.c`, and calls all three.

```c
// stage.c — excerpt linked to the second unit through the declarations the header carries
#include <stdio.h>
#include "stage.h"

static void observe(const char *name, const char *stage, const char *evidence) {
    printf("%-30s%-14s%s\n", name, stage, evidence);
}

int main(void) {
    char buffer[64];

    printf("%-30s%-14s%s\n", "observation", "stage", "evidence");

    observe("header text", "preprocessor", "declarations copied into the source as text");

    snprintf(buffer, sizeof buffer, "the call returned %d", identity());
    observe("function in another unit", "linker", buffer);

    snprintf(buffer, sizeof buffer, "unit declared as %s", unit_name());
    observe("name in another unit", "linker", buffer);

    snprintf(buffer, sizeof buffer, "counter %d", unit_counter);
    observe("variable in another unit", "linker", buffer);

    return 0;
}
```

```
observation                   stage         evidence
header text                   preprocessor  declarations copied into the source as text
function in another unit      linker        the call returned 7
name in another unit          linker        unit declared as choice
variable in another unit      linker        counter 2
```

**LC6.** `stage.h` itself never appears with the `preprocessor` label in any observation; what
appears in its place, the `header text` row, is a result, not `stage.h` itself. What the
preprocessor does amounts to nothing more than copying text: the line `#include "stage.h"` is
replaced with the content of `stage.h` before the compiler ever gets involved. The
`identity(void)` declaration seen in `stage.c` was therefore never hand-typed into `stage.c`'s
source — it arrived there by being copied. The same line was also copied into `choice.c`'s
source; the two copies are independent of one another, and both became part of their own unit.
The `#include` line seen while editing a file is only a summary of the compiled text — the real
source is the expanded text that appears after the preprocessor runs, never seen with the eye.

`choice.c` also includes `stage.h`, and the same three declarations are copied into its source
too. Both units compile with these declarations, and both compile without a warning — the same
declaration entered two separate places and neither was troubled by it, because a declaration
makes a promise, it does not reserve a place. The definition, on the other hand, exists only
once, in `choice.c`; `stage.c` uses all three names but carries the definition of none of them,
borrowing the definitions at the linking stage.

The four rows in the output show how this borrowing works. The `header text` row is an
observation about the source itself, because it proves that a single piece of text carries the
declaration — but what matters is that `stage.c` can use these three names. The `function in
another unit` and `name in another unit` rows reach `choice.c`'s function definitions; the
`variable in another unit` row reaches a variable definition. All three use the same bridge: the
declaration in `stage.h` tells `stage.c` "this name exists, here is its type"; its actual
counterpart arrives only at linking time. While `stage.c` compiles on its own, it does not even
know that `unit_counter`'s value is `2` — all it knows is that some integer like this exists
somewhere.

## What a Header Shares, What It Does Not

`stage.h` carrying only declarations is not a choice, it is a necessity. Since a header file can
be included in multiple units, if a function definition — together with its body — were put
into it, that body would be copied separately into every unit that includes it. Both `stage.c`
and `choice.c` include `stage.h`; if `identity`'s body stood there, both units would have
defined the same name, and this would produce a "same name defined twice" conflict at linking.
This is why the definition must stand only in one unit, not in the header — the include guard
prevents this within a single unit, but it does not prevent a definition from spreading across
multiple units; what prevents that is never putting the definition in the header at all.

This also explains why the line `extern int unit_counter;` is not written as
`int unit_counter = 2;`. The `extern` keyword tells the compiler "do not reserve a place here,
this name already exists elsewhere"; a variable written without it is actually a definition and
reserves a place. Had a variable without `extern` been put in the header, every unit that
includes it would reserve its own copy — `stage.c` and `choice.c` would end up with two separate
`unit_counter`s carrying the same name but standing at different addresses, and this would
produce either a conflict or a silent inconsistency at linking.

This rule does not apply directly to function declarations; a function declaration does not
reserve a place on its own to begin with, it only plays the same role as an `extern` variable
declaration. The essence of the rule is this: every line put in a header must be interpreted the
same way by every unit that reads it. A declaration naturally satisfies this condition, because
it makes the same promise everywhere it is copied; a definition does not, because it demands a
separate place everywhere it is copied.

## The Include Guard

A header file can enter the same unit through more than one path — directly, or from inside
another header file. This does not have to be as obvious a mistake as including a single file
twice; it often arises on its own when two separate headers both depend on a third, shared
header at the same time. When a unit includes both of them, the shared header ends up entering
that unit twice. The `#ifndef` / `#define` pair at the top of `stage.h` exists exactly to cover
this case. The small example below shows this with a header independent of `stage.h`.

```c
// boundary.h — a small header declaring one type; carries an include guard
#ifndef BOUNDARY_H
#define BOUNDARY_H

typedef struct {
    int row;
    int column;
} Position;

#endif
```

```c
// entry.h — an intermediate header pulling in boundary.h through one path
#include "boundary.h"
```

```c
// exit.h — a second intermediate header pulling in boundary.h through another path
#include "boundary.h"
```

```c
// context.c — boundary.h enters the same unit through two separate paths
#include <stdio.h>
#include "entry.h"
#include "exit.h"

int main(void) {
    Position p = {3, 7};
    printf("position: row %d column %d\n", p.row, p.column);
    printf("header pulled in through two paths, type defined once\n");
    return 0;
}
```

```
position: row 3 column 7
header pulled in through two paths, type defined once
```

**LC7.** `context.c` never includes `boundary.h` directly — it pulls it in indirectly, through
two separate paths, via `entry.h` and `exit.h`. The preprocessor processes both `#include`s in
order: in the first, since `BOUNDARY_H` is not yet defined, the `Position` type is copied into
the source and `BOUNDARY_H` is defined; in the second, `#ifndef BOUNDARY_H` is now false, and
everything up to the `#endif` is skipped — `Position` is not copied again. Without the guard,
`Position` would be defined twice in the same unit, and this would not compile. The struct here
is untagged: C counts every untagged struct declaration as a separate type, while a `typedef`
name can only be rebound to the same type. The second copy tries to bind `Position` to a
second, different anonymous type, and the rule is violated. A definition, unlike a declaration,
must be singular.

This shows that the `#ifndef` family solves a problem tied not to declarations but to
definitions. `identity`'s declaration could be copied into two units, because a declaration can
be duplicated. `Position`'s definition cannot be copied twice into the same unit, because a
definition is singular — just like a function's body. The include guard protects a header's own
definitions under this singularity rule.

**LC8.** The name `BOUNDARY_H` itself has no special meaning; it only serves as a marker
answering the question "has this header already been copied." What the preprocessor sees is not
a file name or a path, only whether this name is defined — from the moment the line
`#define BOUNDARY_H` runs, every subsequent `#ifndef BOUNDARY_H` in the same unit gives a false
result. The guard therefore rests not on the file itself but on this single mark left in the
source; if another header with a colliding name used the same mark, the guard would still work,
but this time it would leave out the wrong header. Making the name unique and specific to its
header is therefore the programmer's responsibility, not a guarantee of the language.

`entry.h` and `exit.h` themselves carry no new declaration; both are only bridges to
`boundary.h`. This mirrors, at small scale, a pattern common in real header trees: a header
includes whatever other headers it needs, and the unit that uses it does not have to be aware of
these internal dependencies. `context.c` reached the `Position` type without even knowing
`boundary.h` existed — both `entry.h` and `exit.h` carried this dependency on their own.

## The Bounding Measurement

A header file is not a contract. The declaration in `stage.h` matches the definition in
`choice.c` exactly because, in this core, the two were kept consistent by hand — but the
language itself does not check this consistency. If `stage.h` mistakenly declared
`int identity(int);` instead of `int identity(void);`, the definition in `choice.c` would still
be `int identity(void) { return 7; }`. The compiler compiling `stage.c` looks only at the
declaration in `stage.h` and produces the call accordingly; the compiler compiling `choice.c`
looks only at its own definition. Each is consistent within its own unit, each compiles on its
own without a warning — because the compiler cannot see two units at once, a bound known since
this course's first lesson.

A mismatch can only surface once the two units come together, that is, during linking. But a
classic C linker checks not with which signature a name was declared, only whether the name
exists — it looks up the name `identity` in the symbol table, finds it, matches the address.
Whether the signature matches is often something the linker cannot see either; the error is not
caught there either, it falls into undefined behavior. In the core, the situation is not like
this: the declaration in `stage.h` and the definition in `choice.c` match exactly, and both runs
in this lesson have already proven it — both the `identity` call and the `Position` struct gave
the expected values. A header file does not provide this consistency, it only carries it if it
has already been provided by hand.

This bound is another appearance of the previous lesson's finding, "the stages do not reverse."
The preprocessor, while copying a header, does not ask whether the text is correct, it only
copies. The compiler says nothing as long as it finds the copied text consistent within its own
unit. The inconsistency becomes visible only once the two units are linked — and even then,
only in some cases. This is why a header file's correctness is not something the language
checks, it is something its writer maintains through their own discipline: when the declaration
changes, the definition must change too, even if the two stand in separate files.

How this discipline is maintained is a separate subject — build systems and dependency tracking
are the subject of later curricula — but this lesson's bound says this: `identity`'s signature
does not stay correct automatically just because `stage.h` exists, it can only be changed from a
single place. What a header file actually earns is not a guarantee of correctness, but this
singularity — that when a declaration changes, every unit that uses it sees the same text.

## Summary

- A name appears in the source in two separate forms: a declaration only says it exists, a
  definition gives its body or its place.
- A declaration can be copied into multiple units; a definition can exist in only one unit.
- A header file is not a unit; it only carries text, and it is copied separately into every unit
  that includes it.
- The include guard (`#ifndef` / `#define`) prevents the same header from entering a unit
  through more than one path and copying a definition twice.
- A header file is not a contract: if the declaration and the definition disagree, the compiler
  cannot see it, because it looks at only one unit at a time; the linker, too, looks only for the
  name, not the signature.
- Putting a declaration rather than a definition in a header is a necessity more than a style: if
  the same definition were copied into every unit that includes it, a conflict would arise when
  these units are linked.

## Next Step

This lesson opened up the mechanism behind the `header text` row: the preprocessor copies a
file, but the rest of the source has no way of knowing that this copying happened. The
difference between the `#include "stage.h"` line standing in the source and the expanded text
the compiler sees is the shared property of all of the preprocessor's decisions: none of them
leaves a trace in the source, each exists only through its result. The next lesson returns to
the preprocessor's remaining three decisions — macro value, conditional body, and token
pasting — and shows, with a deeper look than the previous lesson, why each of them works
entirely independently of the compiler at the text level.
