Multi-File Programs and Headers
This chapter brings together declarations, definitions, linkage, and include guards to show how real C programs are organized across multiple files and headers.
Why This Matters
Almost no real C program is a single file. The header/source split — with declarations in .h files and definitions in .c files — is the fundamental organizational pattern of C. Getting it right is what makes large codebases buildable and maintainable.
Prerequisites
c.core.35— linkage, extern, static.c.core.38— conditional compilation and include guards.
Core Concept
The header/source split
- Header (
.h): contains *declarations* (function prototypes,extern
variable declarations, type definitions, macros, and static inline functions) — things that must be shared.
- Source (
.c): contains *definitions* (function bodies, variable
definitions) — the actual code.
Each .c file is a separate translation unit, compiled independently, then linked together.
Public vs. private
- Public API: declarations in headers, with external linkage.
- Private implementation:
staticfunctions and variables in.cfiles,
with internal linkage.
Syntax
A typical structure:
src/
main.c
point.c
include/ (or same dir)
point.h
point.h:
#ifndef POINT_H
#define POINT_H
typedef struct Point {
int x;
int y;
} Point;
Point point_add(Point a, Point b);
double point_distance(Point a, Point b);
#endif
point.c:
#include "point.h"
#include <math.h>
Point point_add(Point a, Point b)
{
Point r = { a.x + b.x, a.y + b.y };
return r;
}
double point_distance(Point a, Point b)
{
double dx = a.x - b.x;
double dy = a.y - b.y;
return sqrt(dx*dx + dy*dy);
}
main.c:
#include "point.h"
#include <stdio.h>
int main(void)
{
Point a = {1, 2}, b = {4, 6};
Point c = point_add(a, b);
printf("%d %d\n", c.x, c.y);
return 0;
}
How It Works
Each .c file is preprocessed (headers expanded), compiled to an object file, and then all object files are linked. The header ensures both point.c and main.c agree on the type and signatures of Point and point_add.
Variations
Opaque types
To hide a struct's definition from users, declare only the type in the header and define it in the source:
/* point.h */
typedef struct Point Point;
Point *point_create(int x, int y);
int point_get_x(const Point *p);
void point_destroy(Point *p);
/* point.c */
struct Point { int x; int y; };
Users can hold a Point * but cannot see or touch the members. This is the core of encapsulation in C.
Header-only libraries
Some libraries put everything in headers as static inline functions. This is convenient but can bloat compile times and binary size.
Common Mistakes
- Defining (not just declaring) a global in a header → duplicate definition.
- Forgetting include guards → duplicate declarations/types.
- Forgetting to compile/link all
.cfiles. - Exposing implementation details that should be
static/opaque.
Undefined Behavior
- Declaring a function with one signature in a header and defining it with a
different signature (incompatible types) is a constraint violation and, if undiagnosed, UB.
- Using an opaque type's incomplete definition where a complete type is
required is a constraint violation.
Portability
- The header/source split is standard and portable.
- Include guards are standard.
- Circular includes are a design problem; forward declarations and careful
structuring avoid them.
Under the Hood
The compiler processes each TU independently; the linker then resolves external symbols. Headers contain no code (except static inline and macros), so they produce no duplicate definitions when included by many TUs.
Practical Usage
- One header per logical module, with an include guard.
- Declare in the header, define in the source.
- Keep private helpers
static. - Use opaque types to enforce encapsulation and reduce rebuild dependencies.
Exercises
1. Split a small program into a module (.h + .c) and a main.c; build and run with multiple TUs. 2. Add a static helper to the module and confirm it is not visible outside. 3. Convert a struct API to an opaque type and update the users. 4. Deliberately define a global in a header and observe the linker error, then fix it with extern.
Deep Challenge
Design a small "bank account" module with an opaque Account type and a public API (account_create, account_deposit, account_withdraw, account_balance, account_destroy), a private static helper, and a header with an include guard. Explain how opaque types and internal linkage provide encapsulation.
Related Concepts
c.lang.linkage— extern/static.c.pp.conditional— include guards.c.build.1— compilation pipeline.
References
- ISO/IEC 9899:2018 §6.2.2 (linkage), §6.7 (declarations), §6.9 (external
definitions).
Verification
- Headers contain declarations, sources contain definitions.
VERIFIED - Include guards prevent double inclusion.
VERIFIED - Opaque types use incomplete struct declarations.
VERIFIED - No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Header/source split
- [ ] Public vs. private APIs
- [ ] Opaque types
- [ ] Forward declarations
- [ ] Circular includes
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.multi.header | 0 | 6 |