C / Intermediate: Memory and Reliable Components / Lesson 11
Modules, Headers, and Reproducible Builds
Give components one source of interface truth and understand how declarations, definitions, and build dependencies fit together.
On this page
Lesson goals
- Separate public headers from private implementation.
- Explain internal and external linkage.
- Diagnose compile and link failures using build inputs.
A component spans declarations and definitions
C17 has no language module system equivalent to modern C++ modules. Programs commonly organize translation units with headers that publish declarations and source files that provide definitions. Each source is compiled independently, so matching declarations are essential; the linker does not perform a complete C type check across arbitrary object files.
Include a componentโs own public header in its implementation. This lets the compiler compare its definition with the same declaration seen by callers. Keep implementation helpers static at file scope to give them internal linkage, and expose only the symbols needed by the interface. A narrow interface reduces accidental dependencies.
Publish a guarded interface
An include guard prevents repeated processing of the same declarations within one translation unit. Choose a project-specific macro name that does not collide with reserved implementation identifiers. Put required type includes in the header itself so the header remains usable without an undocumented include order.
The header snippet is intentionally not a complete executable. It publishes a checked operation whose implementation can remain private. An extern declaration for a shared object does not allocate its definition; ordinarily provide one definition in one source file. Defining a writable global object in a widely included header risks multiple definitions and hidden shared state.
#ifndef COURSE_COUNTER_H
#define COURSE_COUNTER_H
#include <stdbool.h>
bool counter_next(int current, int *out);
#endif
This declares one operation; it still needs a compatible definition and a caller to form a program.
Observe the interface contract in one file
The complete program shows the same declaration and a compatible definition in one file so it is independently runnable. The operation advances a nonnegative count only while the result fits int, and leaves output unchanged on failure. Separating it into counter.c and main.c changes build organization, not that behavior.
The definition should not repeat public documentation inconsistently. Keep the caller-facing contract next to the declaration and use implementation comments for the proof of the guard. A header is an interface agreement, so changing parameter meaning can break callers even when the textual types remain identical.
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
bool counter_next(int current, int *out);
bool counter_next(int current, int *out)
{
if (out == NULL || current < 0 || current == INT_MAX) return false;
*out = current + 1;
return true;
}
int main(void)
{
int next = 0;
if (!counter_next(8, &next)) return 1;
printf("next=%d\n", next);
return 0;
}
next=9
The compatible interface accepts eight and stores nine after validating the range.
Rebuild what the interface change affects
A typical GCC-compatible separate build compiles each .c with -c, then links the resulting object files with the compiler driver. Header changes require recompiling every dependent translation unit. Dependency generation such as -MMD -MP can help a build tool track that relationship, but these options are tool features rather than ISO C syntax.
Use the same important language, ABI, and instrumentation options consistently across units. A stale object file can make a source review disagree with the executable actually running. Reproduce a questionable result in a clean temporary build directory and record source inputs, compiler version, flags, and linked libraries.
Prefer typed operations for repeated behavior
A function-like macro substitutes tokens and can evaluate an argument more than once. A macro such as SQUARE(x) is dangerous when called with an increment expression; parentheses alone cannot repair duplicated evaluation. Prefer a typed function or an appropriate static inline definition when its semantics fit the interface.
Use the preprocessor for conditional configuration and declarations that truly need it. Feature checks should document what changes when a capability is absent. Avoid silently selecting a weaker data format or unsafe algorithm. Review exported names, configuration macros, and generated dependencies as part of the component contract.
Common errors and fixes
A symbol is undefined during linking.
- Why it happens
- The providing object file or library is absent, or its name differs.
- How to fix it
- Inspect the full link command and supply the compatible definition.
- Prevent it next time
- Keep a declared inventory of component sources and libraries.
A shared variable has multiple definitions.
- Why it happens
- A header contains a non-static object definition.
- How to fix it
- Put an extern declaration in the header and one definition in a source.
- Prevent it next time
- Avoid shared writable globals when a context object can express ownership.
A header edit appears to have no effect.
- Why it happens
- Dependent objects were not rebuilt.
- How to fix it
- Regenerate dependencies and rebuild affected units.
- Prevent it next time
- Check dependency files and verify a clean build in isolation.
Check your understanding
1. Split the complete program conceptually into counter.h, counter.c, and main.c. State which include belongs where.
Show worked solution
counter.h contains the guard, stdbool.h, and the declaration. counter.c includes counter.h and limits.h, then defines counter_next without main. main.c includes counter.h and stdio.h, then defines main. Compile both source files and link both objects. The header is included as text by compilation; it is not a substitute for linking counter.cโs definition.
2. Why is SQUARE(i++) a poor interface even if the macro parenthesizes each occurrence of x?
Show worked solution
A textual expansion can contain i++ twice, causing duplicated side effects and potentially unsequenced modifications in a multiplication expression. Parentheses fix grouping only. A function accepting an int value evaluates its argument once before entering the body, though its multiplication still requires a range precondition. Prefer a clear function call with an already validated value.
Key takeaways
- Headers publish contracts; source files provide definitions.
- Linkage controls visibility across translation units, not object lifetime.
- A reproducible build includes dependencies, flags, and libraries.