C / MULTI-FILE PROGRAMS AND BUILDS
Headers as contracts and what belongs in them
Write a header that publishes a module's interface: guards, shared types, prototypes and extern declarations, checked by the compiler on both sides.
What you will learn
- Put declarations, shared types and constants in a header; keep definitions in one .c
- Guard every header so repeated textual inclusion expands to nothing the second time
- Include a module's header in its own .c so prototypes are checked against definitions
- Keep headers self-contained by including the headers for every type they name
Understanding Headers as contracts and what belongs in them
Each .c file is compiled in isolation, so the compiler learns about the rest of the program only from the text it is handed. A header is that text: a written promise that certain functions, types and objects exist somewhere, kept in one file so every consumer is promised exactly the same thing. From that follows the rule for what goes inside. Things that describe belong there (prototypes, typedefs, struct definitions callers must see, enum and macro constants, extern declarations of objects); things that create do not (function bodies and object definitions, which belong to exactly one .c).
A contract only binds if both parties read it. The module's own .c must include its header, because that is the only translation unit that sees the promise and the implementation together, and therefore the only place a mismatch can be diagnosed. A caller that retypes a declaration by hand is believed rather than checked: an object file records the symbol name sum_to, not its parameter or return types, so int against long long, or a swapped pair of arguments, links cleanly and then misbehaves at run time.
Because a header is pasted in textually, it has to survive being pasted more than once and being pasted first. An include guard gives idempotence, so the second expansion is empty and a typedef or struct is not redefined; naming size_t obliges the header to include <stddef.h> itself instead of hoping the consumer already did. Minimality completes the mental model: anything a header exposes becomes something callers may depend on and must be rebuilt for, which is why an incomplete struct plus a handle-returning API is often a better contract than a published layout.
/* temp.h - the contract other files may rely on */
TEMP_H_INCLUDED
TEMP_H_INCLUDED
<stddef.h> /* the header names size_t, so it includes this itself */
TEMP_SCALE_NAME
typedef struct { /* callers create these, so the layout is public */
double lo;
double hi;
} TempRange;
TempRange temp_range(const double *samples, size_t n);
int temp_in_range(const TempRange *r, double v);
extern const char *temp_unit; /* declaration only: no storage made here */
/* TEMP_H_INCLUDED */
/* temp.c - the implementation, checked against the contract */
"temp.h"
const char *temp_unit = "C"; /* the single definition */
TempRange temp_range(const double *samples, size_t n)
{
TempRange r = { 0.0, 0.0 };
if (n == 0)
return r;
r.lo = r.hi = samples[0];
for (size_t i = 1; i < n; i++) {
if (samples[i] < r.lo) r.lo = samples[i];
if (samples[i] > r.hi) r.hi = samples[i];
}
return r;
}
int temp_in_range(const TempRange *r, double v)
{
return v >= r->lo && v <= r->hi;
}
/* main.c - a consumer that knows nothing but the contract */
<stdio.h>
"temp.h"
int main(void)
{
double samples[] = { 12.5, 3.2, 21.7, 8.0 };
TempRange r = temp_range(samples, sizeof samples / sizeof samples[0]);
printf("%s range: %.2f .. %.2f\n", TEMP_SCALE_NAME, r.lo, r.hi);
printf("18.0 in range: %s\n", temp_in_range(&r, 18.0) ? "yes" : "no");
printf("25.0 in range: %s\n", temp_in_range(&r, 25.0) ? "yes" : "no");
printf("unit label: %s\n", temp_unit);
return 0;
}
/* gcc -std=c11 -Wall -Wextra main.c temp.c -o temps && ./temps */A header is the one declaration of what a translation unit offers the rest of the program, written so the compiler, not the linker, can check callers and definitions against it.
Worked examples
An incomplete type as the boundary
A header that names a struct without defining it, so callers can hold a pointer but cannot touch the fields.
/* ring.h */
RING_H_INCLUDED
RING_H_INCLUDED
typedef struct Ring Ring; /* named, not defined: an opaque handle */
Ring *ring_create(void);
void ring_destroy(Ring *r);
void ring_push(Ring *r, int v);
int ring_sum(const Ring *r);
/* ring.c */
<stdlib.h>
"ring.h"
struct Ring { int slot[4]; int next; int count; }; /* private layout */
Ring *ring_create(void)
{
Ring *r = malloc(sizeof *r);
if (r) { r->next = 0; r->count = 0; }
return r;
}
void ring_destroy(Ring *r) { free(r); }
void ring_push(Ring *r, int v)
{
r->slot[r->next] = v;
r->next = (r->next + 1) % 4;
if (r->count < 4) r->count++;
}
int ring_sum(const Ring *r)
{
int s = 0;
for (int i = 0; i < r->count; i++) s += r->slot[i];
return s;
}
/* main.c */
<stdio.h>
"ring.h"
int main(void)
{
Ring *r = ring_create();
if (!r) return 1;
for (int v = 1; v <= 6; v++) ring_push(r, v);
printf("sum of last 4: %d\n", ring_sum(r));
ring_push(r, 10);
printf("after pushing 10: %d\n", ring_sum(r));
ring_destroy(r);
return 0;
}
/* gcc -std=c11 -Wall -Wextra main.c ring.c -o ring && ./ring */Example explained
Line 1typedef struct Ring Ring; declares the tag without a body, which is enough to declare Ring * parameters and return values.
Line 2struct Ring { ... } sits in ring.c, so slot, next and count can be renamed or resized without recompiling main.c.
Line 3ring_create exists because main.c cannot declare a Ring of its own: the size is unknown there.
Line 4Writing sizeof(Ring) or r->count inside main.c is a compile error, so the contract is enforced rather than merely documented.
The definition that does belong in a header
A static inline function in a header gives every including file its own copy, with no duplicate symbol at link time.
/* geom.h */
GEOM_H_INCLUDED
GEOM_H_INCLUDED
static inline int clamp(int v, int lo, int hi)
{
return v < lo ? lo : (v > hi ? hi : v);
}
int clamp_sum(const int *v, int n, int lo, int hi); /* declaration only */
/* total.c */
"geom.h"
int clamp_sum(const int *v, int n, int lo, int hi)
{
int s = 0;
for (int i = 0; i < n; i++) s += clamp(v[i], lo, hi);
return s;
}
/* main.c */
<stdio.h>
"geom.h"
int main(void)
{
int v[] = { -5, 3, 42, 8 };
printf("clamp(42, 0, 10) = %d\n", clamp(42, 0, 10));
printf("clamp_sum(v, 0, 10) = %d\n", clamp_sum(v, 4, 0, 10));
return 0;
}
/* gcc -std=c11 -Wall -Wextra main.c total.c -o clampdemo && ./clampdemo */Example explained
Line 1static inline gives clamp internal linkage in each translation unit, so main.o and total.o each hold a private copy and the linker sees no clash.
Line 2Dropping static and leaving plain inline compiles but fails at link time in an unoptimised build, because a lone inline definition provides no external definition.
Line 3clamp_sum stays a prototype in the header and a definition in total.c, since it is not a one-expression helper worth duplicating everywhere.
Line 4Both files get clamp from the same header, so the body cannot drift the way two pasted copies would.
Why the implementation includes its own header
The return type is part of the contract, and only the file that sees both the prototype and the definition can catch a mismatch.
/* sums.h */
SUMS_H_INCLUDED
SUMS_H_INCLUDED
long long sum_to(int n);
/* sums.c - includes its own header so the compiler compares the two */
"sums.h"
long long sum_to(int n)
{
long long s = 0;
for (int i = 1; i <= n; i++) s += i;
return s;
}
/* main.c */
<stdio.h>
"sums.h"
int main(void)
{
printf("sum_to(100000) = %lld\n", sum_to(100000));
return 0;
}
/* gcc -std=c11 -Wall -Wextra main.c sums.c -o sums && ./sums */Example explained
Line 1sums.c includes sums.h, so changing the definition to return int becomes a compile error in the file that owns the function.
Line 2A caller that skipped the header and wrote int sum_to(int); by hand would still link, because the object file stores the name sum_to and nothing about its types.
Line 35000050000 does not fit in a 32-bit int, which is why the return type is part of the published contract and not an implementation detail.
Line 4Only a translation unit that reads both sides can diagnose the drift, and the header is what makes that possible.
Important notes
Guard names like _TEMP_H_ or __TEMP_H__ live in the identifier space reserved for the implementation; TEMP_H_INCLUDED is safe, and it must be unique across the whole project.
#pragma once works on every mainstream compiler but is not in the C standard, and it decides by file identity, so a header reached through a symlink or a second copied include directory can be read twice; a guard macro suppresses the second expansion no matter which path found the file.
Common mistakes
Writing int log_level = 3; in the header instead of extern int log_level; and defining it in one .c. The first consumer builds, then the link fails with multiple definition of log_level as soon as a second file includes the header.
Retyping a prototype in the caller instead of including the header. Nothing compares the copy with the real function, so when a parameter changes from int to size_t the caller still compiles and passes a wrong value at run time.
Copying util.h into config.h and leaving #ifndef UTIL_H at the top. Whichever header is included second expands to nothing, and the compiler reports unknown type names in a file that plainly includes it.
Try it yourself
Change, predict, then run
In one file, write a guarded block (#ifndef CACHE_H_INCLUDED ... #endif) that declares typedef struct { int hits; int misses; } CacheStats; and double hit_rate(CacheStats s);, then paste that whole block a second time directly below it. Under both copies, define hit_rate and a main that prints the rate for 3 hits and 1 miss; it must still compile with the block duplicated and print 0.75.
Open the C workspaceCheck your understanding
You move the prototypes of api.c's functions into api.h, but api.c itself never includes api.h. Everything still compiles and links. What have you lost?
- Nothing important: the linker compares the definitions in api.c with the prototypes in api.h and rejects a mismatch.
- The compiler no longer compares each definition with the published prototype, so the two can drift apart and callers that trust api.h get undefined behaviour.
- The functions in api.c get internal linkage, so callers that include api.h cannot link against them.
- The prototypes in api.h stop having any effect, so every caller must declare the functions itself.
Show answer
api.c was the only translation unit that could see both the promise and the implementation, so it was the only place a mismatch could be diagnosed; without the include, a definition returning long long against a prototype returning int compiles, links and truncates at run time. The linker option is tempting because the build succeeds either way, but a C object file records only symbol names, giving the linker no types to compare.