C header files and multiple source files
last modified November 14, 2026
A program of more than a few hundred lines is normally split across several
source files. The compiler translates each .c file separately, and
the linker combines the resulting object files into one executable. A header
file carries the declarations that those files share, so the compiler can check
each call without seeing the definitions. This tutorial builds four small
projects: a geometry module, a header included twice, a shared counter and a
private helper function.
Declarations and definitions
A declaration introduces a name and its type. A function prototype such as
double circle_area(double radius); declares the function without
supplying a body. A variable declaration with extern announces an
object that is defined elsewhere. A definition, by contrast, allocates storage
or supplies the function body. A header contains declarations so that several
source files can agree on the same interface; exactly one source file supplies
each definition.
The compiler processes one translation unit at a time: a source file
together with everything it includes after preprocessing. It does not look at
other source files, which is why a function used before its definition needs a
prototype and why a variable shared between files needs an
extern declaration. The linker resolves those references at the
end, after every translation unit has been compiled.
A first multi-file project
The header below declares a macro and two functions. The include guard is
explained in the next section; for now it is enough to know that the
#ifndef block prevents the contents from being processed twice in
one translation unit. The file has no definitions, so it can be included by any
number of source files.
#ifndef GEOMETRY_H #define GEOMETRY_H #define PI 3.14159265358979323846 double circle_area(double radius); double rectangle_area(double width, double height); #endif
geometry.h contains a macro definition and two prototypes. The
prototypes end with semicolons and name the parameter types; a caller only needs
this file to compile. The header does not allocate anything, so including it in
several files cannot create duplicate definitions.
#include "geometry.h"
double circle_area(double radius) {
return PI * radius * radius;
}
double rectangle_area(double width, double height) {
return width * height;
}
geometry.c includes its own header first, which is a useful habit:
it makes the compiler check that the declarations and the definitions agree. The
two functions supply the bodies that the header promised. They use the
PI macro from the header, and neither function needs a prototype
inside this file because the header already provided one.
#include <stdio.h>
#include "geometry.h"
int main(void) {
printf("circle area: %.2f\n", circle_area(2.0));
printf("rectangle area: %.2f\n", rectangle_area(3.0, 4.5));
return 0;
}
The program file includes the standard header for printf and the
local header with double quotes. Quoted includes search the directory of the
including file first, which is where geometry.h lives; angle
brackets are for system headers. The call to circle_area is checked
against the prototype, and the definition is found later by the linker.
$ gcc -std=c17 -Wall -Wextra -o geometry geometry_main.c geometry.c $ ./geometry circle area: 12.57 rectangle area: 13.50
Include guards
Including the same header twice in one translation unit would repeat its
declarations. Repeating a function prototype is harmless, but repeating a
structure definition is an error. An include guard makes the second inclusion a
no-op: the first pass defines the guard macro, and the next pass sees that it is
already defined and skips the body. The macro name must be unique to the header,
and #pragma once is a widely supported non-standard alternative.
#ifndef POINT_H
#define POINT_H
struct Point {
int x;
int y;
};
#endif
POINT_H is defined the first time the file is processed. On a
second inclusion the #ifndef test is false, so the structure
definition between #define and #endif is skipped. The
guard surrounds the whole contents of the header, including any nested includes.
#ifndef DRAW_H #define DRAW_H #include "point.h" void print_point(struct Point p); #endif
draw.h needs the definition of struct Point for its
prototype, so it includes point.h. The guard in
point.h is what makes it safe for a source file to include both
headers directly, or for another header to include draw.h as well.
#include <stdio.h>
#include "draw.h"
void print_point(struct Point p) {
printf("(%d, %d)\n", p.x, p.y);
}
The implementation includes draw.h and therefore also sees
point.h through it. It defines print_point, which
receives a structure by value and prints its two members. The structure
definition comes from the guarded header, not from a second copy in this file.
#include "point.h"
#include "draw.h"
int main(void) {
struct Point p = { 3, 7 };
print_point(p);
return 0;
}
This file includes point.h and draw.h. Without the
guard, the second inclusion would define struct Point again and the
compiler would reject it. With the guard, the structure appears once and both
the local variable and the function prototype are valid.
$ gcc -std=c17 -Wall -Wextra -o point point_main.c draw.c $ ./point (3, 7)
Sharing a variable with extern
A variable that several source files must share is defined once and declared in
a header. The header uses extern, which names the object without
allocating it; the source file contains the definition with an initialiser. If
the header contained the definition instead, every source file that included it
would create its own object and the linker would report multiple definitions.
#ifndef COUNTER_H #define COUNTER_H extern int counter; void increment(void); #endif
The header declares the variable counter and the function
increment. Both names are part of the module's public interface.
The declaration extern int counter; ends with a semicolon and has
no initialiser, so it does not allocate storage.
#include "counter.h"
int counter = 0;
void increment(void) {
counter++;
}
counter.c supplies the one definition int counter = 0;
and the body of increment. Because this file is compiled into the
program exactly once, there is exactly one counter object. The
function changes that object, and every file that includes the header sees the
same variable.
#include <stdio.h>
#include "counter.h"
int main(void) {
increment();
increment();
increment();
printf("counter: %d\n", counter);
return 0;
}
The program calls increment three times and prints the shared
variable. It does not need to know where the definition lives; the declaration
in the header is enough for compilation, and the linker connects the reference
to counter.c. The output is 3.
$ gcc -std=c17 -Wall -Wextra -o counter counter_main.c counter.c $ ./counter counter: 3
Keeping helpers private with static
Not every function in a source file belongs in the public interface. A helper
that is only used inside one module can be marked static, which
gives it internal linkage and hides it from other translation units. The header
then declares only the functions that other files may call. This keeps the interface small and
prevents a helper name from colliding with a function of the same name in
another module.
#ifndef LOGGER_H #define LOGGER_H void log_message(const char *message); #endif
The public header declares only log_message. Callers cannot see or
call the helper that prints the prefix, because its name never appears here.
The include guard protects the declaration if the header is included more than
once.
#include <stdio.h>
#include "logger.h"
static void print_prefix(void) {
printf("[log] ");
}
void log_message(const char *message) {
print_prefix();
printf("%s\n", message);
}
print_prefix is defined with static, so it has internal
linkage. log_message has external linkage and can call the helper
because both are in the same translation unit. Another source file could define
its own print_prefix without conflicting with this one.
#include "logger.h"
int main(void) {
log_message("build started");
log_message("build finished");
return 0;
}
The program includes the public header and calls log_message
twice. It never mentions print_prefix, and the linker never needs
to resolve that name outside logger.c. The output shows the prefix
added by the private helper.
$ gcc -std=c17 -Wall -Wextra -o logger logger_main.c logger.c $ ./logger [log] build started [log] build finished
Separate compilation
Compiling all source files in one command is convenient, but it recompiles
everything whenever one file changes. A larger project compiles each source file
to an object file with -c and links the object files in a second
step. Only the files that changed need a new object file; the linker combines
the existing objects with the new one. The commands below rebuild the geometry
project in two stages.
$ gcc -std=c17 -Wall -Wextra -c geometry.c geometry_main.c $ gcc -std=c17 -Wall -Wextra -o geometry geometry_main.o geometry.o $ ./geometry circle area: 12.57 rectangle area: 13.50
Source
cppreference: translation unit
cppreference: storage duration and linkage
cppreference: #include
GCC: link options
This tutorial covered header files and multiple translation units in C:
declarations versus definitions, a header shared by a module and its caller,
include guards that make repeated inclusion safe, an extern
variable defined in exactly one source file, static helpers with
internal linkage, and separate compilation with object files. The
C functions tutorial explains prototypes and
parameters, the C scope and storage classes tutorial
covers linkage and lifetime, and the C arrays
tutorial shows the data that these modules usually process.
Author
List all C tutorials.