Why this matters
No real C program lives in one file. The Linux kernel has tens of thousands of .c files; even a small tool is usually split into a few modules: parsing, the core logic, output. Splitting code makes it easier to understand (each file has one job), faster to build (only changed files are recompiled), and reusable (a module can move to another project). C's way of doing this is simple and very mechanical, and once you understand it, linker errors stop being mysterious.
Translation units and separate compilation
The compiler only ever looks at one file at a time. A .c file, after the preprocessor has pasted in all its headers, is called a translation unit. Each translation unit is compiled into its own object file, knowing nothing about the others. The linker then joins the object files and resolves references between them.
geometry.h (declarations)
| #include #include |
v v
main.c ----gcc -c----> main.o geometry.c ----gcc -c----> geometry.o
U distance T distance
U midpoint T midpoint
T main U sqrt
\ /
+------- linker (ld) ---------+
| + libc, libm
v
shapes
The header is the contract between them. main.c includes it so the compiler knows how to call distance; geometry.c includes it so the compiler can check that the definitions match that contract.
A complete example
geometry.h, the interface:
#ifndef GEOMETRY_H
#define GEOMETRY_H
struct point {
double x;
double y;
};
double distance(struct point a, struct point b);
struct point midpoint(struct point a, struct point b);
#endif
geometry.c, the implementation:
#include <math.h>
#include "geometry.h"
double distance(struct point a, struct point b)
{
double dx = b.x - a.x;
double dy = b.y - a.y;
return sqrt(dx * dx + dy * dy);
}
struct point midpoint(struct point a, struct point b)
{
struct point m = { (a.x + b.x) / 2, (a.y + b.y) / 2 };
return m;
}
main.c, a user of the module:
#include <stdio.h>
#include "geometry.h"
int main(void)
{
struct point a = {0, 0};
struct point b = {3, 4};
struct point m = midpoint(a, b);
printf("distance: %.1f\n", distance(a, b));
printf("midpoint: (%.1f, %.1f)\n", m.x, m.y);
return 0;
}
Build it in separate steps, so you can see each one:
$ gcc -std=c17 -Wall -Wextra -c geometry.c
$ gcc -std=c17 -Wall -Wextra -c main.c
$ nm geometry.o
0000000000000000 T distance
0000000000000099 T midpoint
U sqrt
$ nm main.o
U distance
0000000000000000 T main
U midpoint
U printf
$ gcc main.o geometry.o -o shapes -lm
$ ./shapes
distance: 5.0
midpoint: (1.5, 2.0)
-c means "compile only, do not link", producing main.o and geometry.o. The U entries in main.o are satisfied by the T entries in geometry.o; sqrt comes from libm and printf from libc. You can also do it in one command, gcc main.c geometry.c -o shapes -lm, which runs the same steps with temporary object files.
If you forget a file, the compiler is still happy (it saw the declarations) but the linker is not:
$ gcc main.c -o shapes
/usr/bin/ld: /tmp/ccSjCV2N.o: in function `main':
main.c:(.text+0x51): undefined reference to `midpoint'
/usr/bin/ld: main.c:(.text+0x84): undefined reference to `distance'
collect2: error: ld returned 1 exit status
What goes in a header
A header is pasted into every file that includes it, so it may only contain things that are fine to repeat in many translation units:
in the header (.h) | in the implementation (.c) |
|---|---|
| function declarations (prototypes) | function definitions (bodies) |
struct, union, enum and typedef definitions | variable definitions: int count = 0; |
macros and constants (#define) | helper functions only this file uses |
extern declarations of shared variables | the main function (in exactly one file) |
#includes the header itself needs | #includes only the implementation needs |
Defining a type in a header is fine because a type creates no storage or code; every file just learns the layout. Defining a variable or a function body there is not, because each including file would get its own copy:
$ cat bad.h
struct point { double x, y; };
int counter = 0;
$ gcc u1.c u2.c -o prog # both include bad.h
/usr/bin/ld: /tmp/ccYQoNV0.o:(.bss+0x0): multiple definition of `counter'; /tmp/cc847cng.o:(.bss+0x0): first defined here
collect2: error: ld returned 1 exit status
Include guards, again
Without a guard, including the same header twice in one file, which easily happens indirectly when a.h and b.h both include point.h, repeats the struct definition:
In file included from twice.c:2:
bad.h:1:8: error: redefinition of 'struct point'
1 | struct point { double x, y; };
| ^~~~~
In file included from twice.c:1:
bad.h:1:8: note: originally defined here
The #ifndef GEOMETRY_H / #define GEOMETRY_H / #endif wrapper makes the second inclusion empty. The guard name must be unique across the project; the usual convention is the file name in upper case with _H. Remember what a guard does not do: it does not stop two different translation units from each getting a copy. That is why it cannot rescue int counter = 0; in a header.
Why the implementation includes its own header
geometry.c needs struct point from the header anyway, but even a module without shared types should include its own header. But the real reason to include it is checking. Suppose the header says double distance(struct point a, struct point b); and someone changes the definition in the .c file to take three parameters. If the .c file includes its header, the compiler sees both and reports conflicting types for 'distance'. If it does not, both files compile, they link fine (the linker only matches names, not types), and calls pass the wrong arguments at run time: undefined behavior. Always include a module's own header first in its .c file.
Organizing a small project
A common layout:
project/
Makefile
src/
main.c
geometry.c
parse.c
include/
geometry.h
parse.h
Built with gcc -Iinclude src/*.c -o shapes -lm. -Iinclude adds the directory to the search path for #include "...". Keep each header minimal: include only what the header itself needs (for example <stddef.h> for size_t), and let .c files include the rest.
Common mistakes
- Function bodies or variable definitions in headers, giving
multiple definitionat link time once two files include the header. - Forgetting a
.cfile on the command line, givingundefined reference. - Definitions that do not match the header, undetected if the
.cfile does not include its own header. - Including
.cfiles (#include "geometry.c"). It seems to work in a two-file program and produces duplicate definitions as soon as a third file does the same. Compile.cfiles, include.hfiles. - Duplicate guard names (two headers both using
UTIL_H): the second header silently disappears, and you get "unknown type" errors that make no sense. - Angle brackets for your own headers:
#include <geometry.h>fails withNo such file or directoryunless you added-I..
Recap
- Each
.cfile plus its headers is a translation unit, compiled alone into an object file; the linker joins the objects. - Headers are the interface: prototypes, types, macros,
externdeclarations. Implementations live in.cfiles. - Anything that creates storage or code must be defined in exactly one
.cfile. - Include guards prevent double inclusion within one translation unit.
- A module's
.cfile includes its own header so the compiler checks that they agree. gcc -ccompiles without linking;nmshows defined (T) and undefined (U) symbols.
Going further: take any program you wrote earlier in this track and split it into a module and a main.c. Then delete the module's #include of its own header, change a parameter type in the .c file only, and see that the program still builds. That experience is the best argument for the rule.