ZetCode

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.

geometry.h
#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.

geometry.c
#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.

geometry_main.c
#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.

point.h
#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.

draw.h
#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.

draw.c
#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.

point_main.c
#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.

counter.h
#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.

counter.c
#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.

counter_main.c
#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.

logger.h
#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.

logger.c
#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.

logger_main.c
#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

My name is Jan Bodnar, and I'm a dedicated programmer with a deep passion for coding. Since 2007, I've been sharing my expertise through over 1,400 articles and 8 e-books. With more than a decade of teaching experience, I strive to make programming accessible and engaging.

List all C tutorials.