Modules and translation units
C does not have a module system. What it has is a translation-unit model: each source file is compiled independently to an object file, and the linker combines object files into an executable or library. Visibility, sharing, and code organisation across files are managed by the conventions of headers — interface descriptions textually included into the source files that need them — and by the rules of linkage that the standard specifies. The model is older than every language-level module system in commercial use, and it has been retained in C because it is simple, well-understood, and adequate for the language’s principal use cases. It is also the source of several of C’s distinctive build-time challenges: the include-order sensitivity, the difficulty of separating declaration from definition, and the link-time errors that surface only after the type checker is satisfied.
The compilation pipeline
A C program is built in four conventional stages, each with a distinct tool:
- Preprocessing —
#includedirectives are expanded, macros are substituted, conditional compilation is resolved. Output: a single token sequence per source file. - Compilation — the token sequence is parsed, type-checked, optimised, and translated to assembly. Output: an assembly file (
.s). - Assembly — the assembly is translated to a relocatable object file. Output: an object file (
.oon Unix,.objon Windows). - Linking — the object files, plus any libraries, are combined into an executable or shared library. Output: an executable, a static library (
.a/.lib), or a shared library (.so/.dylib/.dll).
Each stage may be invoked individually with the appropriate flag:
cc -E file.c -o file.i # preprocess only
cc -S file.c -o file.s # preprocess + compile to assembly
cc -c file.c -o file.o # preprocess + compile + assemble
cc file.o -o file # link
Conventionally cc file.c -o file performs all four stages. The separability matters chiefly because each stage operates on different units: preprocessing operates on a single source file plus its includes; compilation operates on the resulting translation unit; linking operates on all the object files together.
Source files, object files, executables
A source file contains the C text that the programmer writes. By convention it has the extension .c.
A translation unit is the result of preprocessing one source file. The compiler sees one translation unit at a time; it has no knowledge of any other source file.
An object file is the compiled, relocatable form of one translation unit. It contains machine code for each function, data for each defined object, and a symbol table listing the names defined in the file (with external linkage) and the names referenced from elsewhere (the unresolved symbols).
An executable (or shared library, or static library) is the result of combining object files. The linker resolves each unresolved symbol to a defining symbol in some object file or library; if any symbol cannot be resolved, the link fails.
Headers and the inclusion model
A header file (conventionally with extension .h) contains declarations: function prototypes, type definitions, extern object declarations, macro definitions. A .c file that needs to call a function defined in another .c file #includes the header that declares it.
/* point.h */
#ifndef POINT_H
#define POINT_H
typedef struct { double x, y; } point;
double point_distance(point a, point b);
#endif
/* point.c */
#include "point.h"
#include <math.h>
double point_distance(point a, point b) {
double dx = a.x - b.x, dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
/* main.c */
#include "point.h"
#include <stdio.h>
int main(void) {
point a = {0, 0}, b = {3, 4};
printf("%g\n", point_distance(a, b));
}
Both point.c and main.c #include "point.h" so that the same declarations are visible in both. The compiler can verify that the call in main.c matches the prototype, and the linker can resolve the call to the definition in point.c.
The inclusion model has several practical consequences:
- Header guards are required. Without them, transitive inclusion produces redefinitions.
- Compilation costs scale with header size. Each translation unit re-parses every header it includes; a project with many translation units and large headers re-parses the same content many times. Precompiled headers and forward declarations are the conventional mitigations.
- Headers must declare what users need and only what they need. A header that includes another header forces every user of the first to also process the second; controlling header dependencies is a non-trivial part of large-project maintenance.
Conventionally a header declares an interface; the corresponding source file implements it. Code with no external interface lives in a .c file with no header.
Visibility: static at file scope
A function or variable declared static at file scope has internal linkage: it is visible only within the translation unit that declares it. The keyword is the principal mechanism for module-internal helpers in C:
/* logger.c */
#include "logger.h"
#include <stdio.h>
static FILE *log_file = NULL; /* internal: not visible outside */
static void open_default(void) { /* internal helper */
log_file = stderr;
}
void log_message(const char *msg) { /* external: declared in logger.h */
if (!log_file) open_default();
fputs(msg, log_file);
}
The convention extends to data: globals that should be private to one source file are declared static. The discipline reduces the surface area of the link-time symbol table and prevents accidental name collisions.
Linkage and the One Definition Rule
The standard requires that each object or function with external linkage have at most one definition across all linked translation units. This is the One Definition Rule. Multiple declarations are permitted; multiple definitions are not. The linker’s diagnostic for a violation — multiple definition of 'foo' — is the typical evidence.
The conventional discipline:
- Define each object or function in exactly one source file.
- Declare each in a header that other source files include.
- Include the header in the defining source file too, so the compiler can check that declaration and definition agree.
The third point catches a subtle class of bugs: a .c file that defines void foo(int) but a header that declares void foo(double). Without the third rule, both files compile cleanly; the disagreement surfaces only when a caller’s code is bound to the wrong implementation, with consequences that depend on the calling convention.
For full treatment of scope, linkage, and storage duration, see Scope and linkage. The preprocessor, which performs the textual inclusion that makes this whole model work, is treated separately in Preprocessor.
Static and shared libraries
A static library is an archive of object files with an index. The linker pulls in each member that defines a symbol referenced by the program; unreferenced members are dropped. The conventional Unix toolchain:
cc -c -o foo.o foo.c
cc -c -o bar.o bar.c
ar rcs libutil.a foo.o bar.o # build the static library
cc main.c -o main -L. -lutil # link against it
The -L. flag adds the current directory to the library search path; the -lutil flag asks the linker to find libutil.a (or libutil.so) and link against it.
A shared library (also called a dynamic library) is loaded at program start (or on demand) and shared by every running process that uses it. Building one:
cc -fPIC -c foo.c bar.c
cc -shared foo.o bar.o -o libutil.so
-fPIC produces position-independent code, which a shared library requires; -shared instructs the linker to produce a shared object rather than an executable. The runtime resolution of shared libraries is handled by the system’s dynamic loader (ld.so on Linux); the search path is configured through LD_LIBRARY_PATH, the runtime-link-time -rpath flag, and the system-wide /etc/ld.so.conf.
The trade-offs:
- Static libraries produce self-contained executables: deployment is a single file, but every program that uses the library has its own copy.
- Shared libraries reduce the per-process memory footprint when many programs use the same library, and permit the library to be updated without rebuilding the consumers.
Build systems
The C standard does not specify a build system. Compiling a non-trivial program is the responsibility of the programmer or the build tool of choice. The conventional Unix mechanism is make, with a Makefile describing how each output is produced from its inputs:
CC := cc
CFLAGS := -Wall -Wextra -O2 -std=c11
OBJECTS := main.o point.o util.o
program: $(OBJECTS)
$(CC) $(CFLAGS) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJECTS) program
The contemporary alternatives are cmake (cross-platform; generates Makefiles or other build files), meson (declarative; emphasises speed), and the older autotools (./configure && make). For very small projects, a shell script or a single cc *.c invocation suffices.
Header organisation in practice
A medium-sized project typically distinguishes:
- Public headers, declaring the interface the project exposes to its consumers. These live in a directory included by users (
include/myproject/). - Internal headers, declaring interfaces shared among the project’s own source files but not intended for users. These live alongside the source (
src/). - No header at all for source files whose contents are entirely internal and whose only external interface is through other modules.
myproject/
├── include/
│ └── myproject/
│ ├── core.h
│ └── logger.h
├── src/
│ ├── core.c
│ ├── core_internal.h
│ ├── logger.c
│ └── util.c (no header — internal to logger.c only)
├── tests/
│ └── core_test.c
└── Makefile
The arrangement separates concerns: the public headers are the API; the internal headers are implementation-shared declarations; the source files are the implementation. Consumers see only what is in include/.
Forward declarations
A forward declaration introduces a name without defining it. The technique reduces compilation cost and breaks circular dependencies between headers:
/* a.h */
struct b; /* forward declaration */
void a_use(struct b *p); /* legal: pointer to incomplete type */
/* b.h */
struct a;
struct b { struct a *backref; }; /* legal: pointer to incomplete type */
/* …include both elsewhere… */
The principle: pointers to a structure may appear in code that has only seen the structure’s name; concrete uses (member access, sizeof) require the full definition. Many headers declare typedef struct foo foo; and use foo * throughout, with the structure definition exposed only in the source file or in a separate “internal” header.