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.

  1. Include a standard header when using its declared library functions.
  2. Put declarations that need to be shared in your own header.
  3. 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:

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.


Suggested topics

0% read