Skip to content
VLSI Mentor

C Part I · Module 11 · Building Multi-File C Programs

Header Files and Include Guards

A header exists to be included by many files, and the moment headers start including other headers it will also be included more than once by a single file — which, since a header is text, means its contents arrive twice. There is a standard mechanism for preventing that, it is three lines long, and it is the part of this subject everybody teaches. The harder half is the editorial one: deciding what belongs in a header at all. That question has a rule behind it rather than a convention, the rule follows from two things you already know, and getting it wrong produces failures the guard cannot touch — including one measured here that compiles, links, runs, and silently gives two files two separate copies of something the author wrote once.

Intermediate20 min readCPart IFundamentalsBuild

Part I · Module 11 · Chapter 11.3 · Header Files and Include Guards

1. The Problem

Two headers, each perfectly reasonable, each including what it needs:

Azvya Education Pvt. Ltd.VLSI Mentor
point.h
struct point { int x; int y; };
Azvya Education Pvt. Ltd.VLSI Mentor
shape.h
#include "point.h"
struct shape { struct point origin; int kind; };
Azvya Education Pvt. Ltd.VLSI Mentor
line.h
#include "point.h"
struct line { struct point a; struct point b; };

Now a file that wants both:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    #include "shape.h"
    #include "line.h"
    int main(void) { return 0; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
In file included from g_main.c:2:
In file included from ./line.h:1:
./point.h:1:8: error: redefinition of 'point'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc -std=c17 -E g_main.c | grep -c "struct point { int x; int y; };"
2

The text is in the translation unit twice, and Chapter 11.2 §4 established why that is fatal for a type definition.

The two routes by which one header reaches a single translation unit. On the first route, main.c includes shape.h, shape.h includes point.h, and point.h's text is pasted in as copy one. On the second route, main.c includes line.h, line.h also includes point.h, and point.h's text is pasted in again as copy two, producing a redefinition error. Neither header did anything wrong; each included what it needed.main.c includesshape.hthe first routeshape.h includespoint.hit needs struct pointpoint.h text, copy 1pasted inmain.c includesline.hthe second routeline.h includespoint.hit needs struct point toopoint.h text, copy 2redefinition of 'point'12
Figure 1 — the two routes by which one header reaches a single translation unit. Neither header did anything wrong: shape.h needs struct point and says so, line.h needs it and says so, and main.c is entitled to want both. The duplication is not a property of any one file — it is a property of the combination, and the combination is chosen by whoever writes main.c. That is why the fix cannot live in shape.h or line.h or main.c. It has to live in point.h, which is the only file present on both routes.

2. The Include Guard

Azvya Education Pvt. Ltd.VLSI Mentor
point.h, guarded
#ifndef POINT_H
#define POINT_H

struct point { int x; int y; };

#endif /* POINT_H */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc -std=c17 -Wall -Wextra -Wpedantic -c g_main.c -o /dev/null
$ cc -std=c17 -E g_main.c | grep -c "struct point { int x; int y; };"
1

Compiles, and the text appears once.

The mechanism is Chapter 11.2's preprocessing, used deliberately. The first time the file is reached, POINT_H is not defined, so the #ifndef is true, the body is included — and the body's second line defines POINT_H. The second time, POINT_H is defined, the #ifndef is false, and everything down to #endif is deleted before the compiler sees it.

Four rules for writing one:

Guard every header, without exception. Not just the ones you think are at risk — you cannot know which file will include which, and the diamond in §1 was assembled by main.c, not by either header.

Make the macro name unique. POINT_H is fine in a small project and a collision waiting to happen in a large one; MYPROJ_POINT_H is better. A collision is silent and nasty: the second header is deleted entirely and you get errors about missing types with no indication why.

Put #define immediately after #ifndef, before anything else. If a header includes another header before defining its own guard macro, a cycle can get past it.

Nothing outside the guard. Not a comment that matters, not an include, not a #pragma. Everything between #ifndef and #endif.

#pragma once

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    #pragma once
    struct point2 { int x; int y; };
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc -std=c17 -Wall -Wextra -Wpedantic -c g_p2.c -o /dev/null
$
$ cc -std=c17 -E g_p2.c | grep -c "struct point2"
1

It works, and it is shorter and immune to macro-name collisions. Two honest caveats:

It is not ISO C. It is a universally implemented extension, and it is not in the standard.

And -Wpedantic did not flag it. Verified above — no diagnostic at all. So you cannot rely on the compiler to tell you when you have stepped outside the standard here; you have to know.

Use whichever your project uses, consistently. If you have no existing convention and portability to unusual toolchains matters, #ifndef is the conservative choice.

3. What a Guard Does Not Do

This is the section that matters most, and the measurement is unambiguous.

Azvya Education Pvt. Ltd.VLSI Mentor
bad.h — guarded, and still wrong
#ifndef BAD_H
#define BAD_H
int twice(int x) { return 2 * x; }     /* a DEFINITION in a header */
#endif

Two .c files include it. Each compiles perfectly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ cc -std=c17 -Wall -Wextra -Wpedantic -c ba.c -o ba.o
$ cc -std=c17 -Wall -Wextra -Wpedantic -c bb.c -o bb.o
$

The guard did its job. In each translation unit, twice was defined exactly once.

Then link:

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

An include guard protects one translation unit from seeing a header twice. It can do nothing about two different translation units each seeing it once. Each .c file is preprocessed separately (Chapter 11.2 §5), so each gets its own fresh set of macros and its own copy of the header's contents.

The guard and the what-goes-where rule solve different problems. The guard handles repetition within a unit. Only the rule in §4 handles repetition across units — and Chapter 11.1 §3 already gave you the principle: a definition may exist exactly once in the whole program.

4. What Belongs in a Header

The rule follows from two things you have:

  • Chapter 11.1: a declaration may repeat; a definition may not.
  • Chapter 11.2: a header's text appears in every translation unit that includes it.

Put them together and the test is mechanical: if this construct appeared once in every .c file of the project, would that be correct? If yes, it belongs in a header.

ConstructHeader?Because
function declaration — int add(int, int);yesa declaration; repeats harmlessly
object declaration — extern int count;yesa declaration; creates nothing
struct / union / enum definitionyesproduces no symbol (11.1 §2); each unit needs its own copy to know the layout
typedefyescreates a name, not an object
macro — #define MAX 64yesthe preprocessor handles it; never reaches the linker
function definitionnoone symbol per unit → duplicate symbol (§3)
object definition — int count = 0;nosame
static anythingnoone private copy per unit — §5
#include of what the header needsyes§6

The third row is the one that looks inconsistent and is not. A struct definition is a definition and yet belongs in a header, because Chapter 11.1 §2 measured that it produces no symbol: it is information for the compiler, needed identically by every unit, and invisible to the linker. The "exactly one definition" rule is about things that exist at run time, and a type is not one of them. Its restriction is the one the guard handles — once per translation unit.

5. The static Trap

The static row in that table deserves its own treatment, because it is the failure that produces no diagnostic at all.

Azvya Education Pvt. Ltd.VLSI Mentor
counter.h — guarded, and silently wrong
#ifndef COUNTER_H
#define COUNTER_H
static int call_count = 0;
static void bump(void) { call_count++; }
static int  reads(void) { return call_count; }
#endif

Two .c files include it. One calls bump twice, the other once. Then both report what they see:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ./staticdup
a sees 2, b sees 1

Not "3" in both. Two separate counters. The symbol tables show exactly what happened:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ nm ca.o                  $ nm cb.o
T _a_count                 T _b_count
T _a_work                  T _b_work
t _bump                    t _bump
b _call_count              b _call_count
t _reads                   t _reads

Every object file got its own call_count, its own bump, its own reads — all lowercase, all private to their unit. The program builds without a single warning and the shared state is not shared.

6. A Header Must Stand on Its Own

Azvya Education Pvt. Ltd.VLSI Mentor
shape.h — correct, because it includes what it uses
#ifndef SHAPE_H
#define SHAPE_H

#include "point.h"               /* shape.h uses struct point, so it includes it */

struct shape { struct point origin; int kind; };

#endif

A header must compile when it is the first thing a file includes. If it uses a type, it includes the header that defines that type. If it uses size_t, it includes <stddef.h>. It never relies on the including file having included something first.

The alternative — "include point.h before shape.h" — is an unwritten ordering requirement that every user must know and that breaks the moment someone reorders their includes alphabetically. Guards make self-containment free: including point.h from twenty headers costs nothing, because nineteen of those inclusions evaporate.

There is a one-line way to check it. In shape.c, make #include "shape.h" the first line. Then if shape.h forgot an include, shape.c fails to compile and you find out immediately, in the file that owns the header.

Two smaller conventions, stated once:

#include "local.h" versus #include <system.h>. Quotes search your project first and then the system paths; angle brackets search only the system paths. Use quotes for your own headers and angle brackets for the standard library. Where exactly each form searches is the implementation's business, and configuring it is Chapter 11.10's.

Prefer a forward declaration to an include when a pointer is all you need. If shape.h only ever uses struct point *, then struct point; is enough — an incomplete type (Chapter 10.2 §4) — and the header does not have to include point.h at all. This keeps dependency chains short, which matters for rebuild times in a large project.

7. A Worked Pair

Azvya Education Pvt. Ltd.VLSI Mentor
counter.h
#ifndef MYPROJ_COUNTER_H
#define MYPROJ_COUNTER_H

#include <stddef.h>              /* for size_t, used below */

#define COUNTER_MAX 1000         /* macro: fine in a header       */

typedef struct counter counter;  /* an incomplete type, named     */

struct counter_stats {           /* a type definition: no symbol  */
    size_t hits;
    size_t misses;
};

extern int counter_verbose;      /* DECLARATION of a shared object */

void              counter_bump(counter *c);          /* declarations */
struct counter_stats counter_read(const counter *c);

#endif /* MYPROJ_COUNTER_H */
Azvya Education Pvt. Ltd.VLSI Mentor
counter.c
#include "counter.h"             /* FIRST — proves the header is self-contained */

int counter_verbose = 0;         /* the one DEFINITION of the shared object */

struct counter {                 /* the type's definition stays here:       */
    size_t hits;                 /* callers hold counter * and never need   */
    size_t misses;               /* to know what is inside                  */
};

static size_t total_bumps = 0;   /* genuinely private to this file          */

void counter_bump(counter *c) { c->hits++; total_bumps++; }

struct counter_stats counter_read(const counter *c)
{
    struct counter_stats s = { .hits = c->hits, .misses = c->misses };
    return s;
}

Six decisions in that pair, each traceable to a rule:

The guard name is prefixed. §2.

counter.h includes <stddef.h> because it uses size_t. §6.

counter.c includes counter.h first. §6's self-containment check.

struct counter is declared in the header and defined in the .c. Callers get a named incomplete type — Chapter 10.9 §5's handle pattern — so they can hold counter * without seeing the members, and changing the members does not force them to rebuild.

counter_verbose is extern in the header and defined once in the .c. Chapter 11.1 §5's portable pattern.

total_bumps is static in the .c, not the header. §5.

8. What You Should Be Able to Say Now

  1. A header is text, so two routes to it put it in a translation unit twice — measured, and fatal for a type definition.
  2. The guard goes in the file that is reached twice, because the duplication is a property of the combination, not of any one file.
  3. #ifndef / #define / #endif, guarding the whole file, with the #define immediately after, and a unique prefixed name.
  4. #pragma once works and is not ISO C — and -Wpedantic did not flag it.
  5. A guard protects one translation unit, not two — measured: both files compiled clean and the link failed with duplicate symbol '_twice'.
  6. The test for a header: would this be correct if it appeared once in every .c file?
  7. Declarations, type definitions, typedefs and macros: yes. Function and object definitions: no.
  8. A struct definition belongs in a header because it produces no symbol — the one-definition rule is about things that exist at run time.
  9. static in a header gives each file a private copy — measured, "a sees 2, b sees 1", with b _call_count in both objects and no diagnostic at all.
  10. The exceptions are static const values whose address is never taken and static inline functions.
  11. A header must compile first — include what it uses; make #include "own.h" the first line of the matching .c to check.
  12. Prefer a forward declaration to an include when a pointer is all you need.

9. Exercises

Exercise 1 — write the guard. Write sensor.h with a guard, a struct sensor definition, a typedef, a macro SENSOR_MAX, and a declaration of void sensor_reset(struct sensor *). Say why each item is allowed to be there.

Exercise 2 — header or source. For each, say where it goes and which rule decides.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int frame_count = 0;
extern int frame_count;
struct frame { int id; };
typedef struct frame frame;
int next_id(void) { return ++frame_count; }
int next_id(void);
static int scratch[64];
#define FRAME_MAX 32
static const int FRAME_LIMIT = 32;

Exercise 3 — predict the failure. For each mistake, say whether it is a compile error, a link error, or a silent wrong answer, and name the measurement in this chapter that settles it.

  • a type definition in an unguarded header, included twice by one file
  • a function definition in a guarded header, included by two files
  • a static object in a guarded header, included by two files

Exercise 4 — the guard that did nothing. Explain in three sentences why bad.h's guard worked perfectly and the program still failed to link.

Exercise 5 — find the shared state. A project has static FILE *log_handle = NULL; in log.h, included by eleven .c files. Describe the bug precisely, say what nm would show, and give the fix as a header/source pair.

Exercise 6 — make it self-contained. shape.h uses struct point and size_t and includes neither. Give the two lines that fix it, and the one-line change to shape.c that would have caught the omission.

Exercise 7 — shorten the dependency. widget.h includes engine.h only because it declares void widget_attach(struct engine *e);. Give the change that removes the include, say what it costs the caller, and name the chapter that covers the type you used.

Exercise 8 — audit a header. Given the following, list every problem and rewrite it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include "util.h"
#ifndef UTIL2
#define UTIL2
static int counter;
int helper(int x) { return x + counter; }
struct util { int a; };
#endif
int extra;

10. Summary

  • A header is text; two routes put it in one unit twice — measured, redefinition of 'point'.
  • Guard the file that is reached twice, whole-file, #define first, unique prefixed name.
  • #pragma once works, is not ISO C, and -Wpedantic stays silent about it.
  • A guard protects one translation unit only — both files compiled clean and the link failed: duplicate symbol '_twice'.
  • The test: would this be correct once in every .c file?
  • Yes: declarations, type definitions, typedefs, macros. No: function and object definitions, and static anything.
  • static in a header is the dangerous one — "a sees 2, b sees 1", no diagnostic. Exceptions: static const values, static inline functions.
  • Headers must be self-contained; include your own header first in the matching .c to prove it.
  • Prefer a forward declaration when a pointer is all you need.

The header pattern is now complete except for one word that has been doing unexplained work in every section. extern appeared in the shared-object declaration and was described as "the declaration, not the definition". static appeared twice with two apparently unrelated meanings — once making a file-scope object private to its file, and once, back in Chapter 9.3, making a local object survive between calls. Those are not two uses of one idea; they are one keyword spelling two genuinely different properties, and C reused it because the two situations never overlap. The next chapter is about what extern and static actually control at file scope — the property that decides whether a name in one file and the same name in another refer to one object or to two — and it is the mechanism every rule in this chapter has been quietly relying on.

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.