Chapter 14 of 24
14. The preprocessor and header files
Text preparation before compilation
The preprocessor handles lines that begin with # before the compiler checks ordinary C code. The most familiar directive, #include, makes a header's declarations available to a source file; it does not paste a compiled library into the program.
- Include a standard header when using its declared library functions.
- Put declarations that need to be shared in your own header.
- Add an include guard so repeated inclusion does not process the same header contents multiple times.
Macros are textual substitutions, not ordinary typed functions. Learn the include-and-declaration workflow first, then use macros only when their benefits and evaluation behavior are clear.
The preprocessor runs before compilation. Common directives:
#include: inserts declarations from a header.#define: defines a macro or symbolic constant.#if,#ifdef,#ifndef,#else,#endif: conditional compilation.#error: emits a build-time error.
Use include guards so a header can safely be included more than once:
#ifndef POINT_H
#define POINT_H
struct Point {
double x;
double y;
};
#endif
Modern compilers also commonly support #pragma once, but it is not part of
the C standard. Prefer const objects and functions over macros when possible:
macros do not have normal type checking, and their arguments may be evaluated
more than once.
Headers generally contain declarations, type definitions, and macros, not
ordinary function definitions or non-static object definitions. A source
file includes its own header when it implements that header's interface.
What belongs in a header?
A header is a contract for code that uses a module. It can declare a function,
define a structure needed by callers, or declare an enum and constants. Keep
private helper functions and implementation-only details in the .c file.
Including a header provides declarations; it does not by itself compile or
link the matching implementation.
// point.h
#ifndef POINT_H
#define POINT_H
struct Point {
double x;
double y;
};
double point_distance_from_origin(const struct Point *point);
#endif
The header exposes a type and function declaration. Its implementation
belongs in a .c file that includes "point.h".
Include guards prevent repeated inclusion within one translation unit. Use a
distinct guard name based on the project and file to reduce collisions. When a
header depends on another type, include the header that defines that type
instead of relying on the caller to include files in a lucky order.
Macros are substituted by the preprocessor before the compiler checks normal
types. Parenthesizing macro parameters and the whole replacement can prevent
some precedence mistakes, but cannot make a macro behave exactly like a
function. For example, a macro that uses an argument twice may evaluate an
expression twice. Prefer a function for ordinary computation; use macros for
conditional compilation, include guards, and cases where compile-time
substitution is genuinely needed.
Checkpoint: Explain why a header declaration does not provide a function
body, and name two items that usually belong in a public header.
Macros: useful tools with sharp edges
An object-like macro can name a compile-time expression:
#define ARRAY_COUNT 8
int main(void)
{
int values[ARRAY_COUNT] = {0};
return values[0];
}
A function-like macro uses parameters, but substitution is textual:
#define SQUARE(x) ((x) * (x))
int main(void)
{
return SQUARE(3) == 9 ? 0 : 1;
}
Even with parentheses, SQUARE(i++) increments i twice, so it is not
equivalent to a normal function call. Prefer a typed function for ordinary
behavior. Macros are appropriate for include guards, conditional compilation,
and carefully designed interfaces that truly require preprocessing.
Use #if for numeric preprocessor conditions and #ifdef when checking
whether a macro is defined. Be aware that the preprocessor does not understand
C types or variable scope. Keep conditional compilation narrow so each
supported configuration remains buildable and testable.