Header Organization and Modular Builds
What This Concept Is
A healthy C project looks like:
src/
main.c
list.c
list.h
buffer.c
buffer.h
buffer_internal.h (optional, shared only among buffer's own .c files)
Each .c file compiles to one .o. Headers are grouped into:
- Public header (
list.h) - what callers need: declarations of functions, opaque type forward declarations (typedef struct list List;), and minimalstructdefinitions only when inlining or direct access is genuinely part of the API. - Private / internal header (
list_internal.h) - shared between the module's own.cfiles, never shipped to callers. - Module source (
list.c) - the one place each public function is defined.
Guidelines: include only what you use; never include from a header if a forward declaration suffices; keep the include tree shallow.
Why It Matters Here
C has no module system. The compiler only knows what you paste into each translation unit. Sloppy headers produce three recurring failures:
- compile time explodes because every
.ctransitively includes too much - recompiles are too broad because a small change to a header rebuilds unrelated files
- API boundaries leak because struct internals are visible to callers who then depend on them
Module 4 and your future jobs assume you can keep a modular build clean without tools.
Concrete Example
A simple list module with an opaque type.
list.h:
#ifndef LIST_H
#define LIST_H
#include <stddef.h>
typedef struct list List; /* opaque: caller cannot see members */
List *list_create(void);
void list_destroy(List *l);
int list_push(List *l, int value);
int list_get(const List *l, size_t i, int *out);
size_t list_len(const List *l);
#endif
list.c defines struct list { int *data; size_t len; size_t cap; }; privately and all five functions. main.c uses list.h but cannot touch l->data, because struct list is hidden in list.c. Change the internal representation later and callers do not need to recompile for that reason alone.
Common Confusion / Misconception
"Headers should include everything the .c file uses." No. A header should include only what the header itself needs to compile (types it mentions in its declarations). The .c file then adds any other includes it needs. This keeps callers from paying for dependencies they do not use.
"I need to include list.h from buffer.h because buffer uses a List *." Usually you only need a forward declaration: typedef struct list List; inside buffer.h. Include list.h in buffer.c where you actually call list functions.
"One big common.h is fine." It is fine in a 3-file toy. In a 30-file project it becomes the slowest thing to compile.
How To Use It
Authoring rules:
- One header per module. Name it after the module.
- Every header has a unique
#ifndef MODULE_H / #define MODULE_H / #endifguard. - Header includes only what it needs for its own declarations. Prefer forward declarations.
- Public structs only if accessors and size are stable; otherwise opaque.
- The module's own
.cfile#includes its own header first, then standard and then other local headers. - Avoid circular includes. If A needs B and B needs A, factor shared declarations into a third header.
Check Yourself
- When is a forward declaration enough and when do you need the full include?
- What is an opaque type and why is it useful?
- Why does including a big header in another header slow down builds?
Mini Drill or Application
- Take the
listexample and split it further: addlist_internal.hthat declares a helperstatic inline int list_grow(List *l, size_t n);usable only fromlist.c. Confirmmain.ccannot see it. - Introduce
buffer.h/buffer.cthat usesList *only via pointer. Use a forward declaration inbuffer.h. - Measure build times: one big
common.hvs per-module headers. Usetime gcc -c .... - Reorder the includes inside
list.cand explain the rationale for "own header first."