Chapter 12: Preprocessor and Compilation
C code goes through several transformations before it becomes a program you can run. The preprocessor expands macros and includes headers, the compiler turns the resulting translation unit into object code, and the linker resolves external references to build an executable or a library. This chapter focuses on guiding that pipeline by using macros, controlling what gets compiled, organizing headers safely, and understanding how each stage works so build problems are easier to diagnose. We finish by situating the C Standard Library within this pipeline, since its headers and symbols are resolved through the same steps.
Using macros and #define effectively
The directive #define introduces a macro that the preprocessor replaces before compilation. Macros can be simple constants or parameterized templates. Prefer const variables and functions for most logic; reach for macros where you need compile time switches, small inlined expressions, or conditional platform adaptations.
#include <stdio.h>
/* object-like macro */
#define PI 3.14159265358979323846
/* function-like macro with parentheses to avoid precedence pitfalls */
#define SQR(x) ((x) * (x))
/* stringizing and token pasting */
#define STR(x) #x
#define CAT(a,b) a##b
int main(void) {
int CAT(val, 1) = 7; * becomes int val1 = 7; */
printf("%s = %d\n", STR(val1), val1);
printf("Area scale: %f\n", PI * SQR(2));
return 0;
}
Parenthesizing and scoping carefully
Always wrap macro parameters and the whole expansion in parentheses to preserve intent when the macro is used in a larger expression. Avoid side effects in arguments because a macro may evaluate them more than once. Use do { … } while (0) to package multi statement macros safely.
#define LOG(fmt, ...) do { \
fprintf(stderr, "[log] " fmt "\n", __VA_ARGS__); \
} while (0)
Leveraging predefined macros
Compilers define helpful macros such as __FILE__, __LINE__, and feature test macros for platforms and compilers. Use them sparingly to tag messages or select code paths when necessary.
#define HERE __FILE__ ":" STR(__LINE__)
#define STATIC_ASSERT(cond, msg) typedef char static_assert_##msg[(cond) ? 1 : -1]
inline functions for typed behavior where performance matters and keep macros small and predictable.
Conditional compilation
Conditional compilation lets you include or exclude code at preprocess time. This is useful for platform differences, debug builds, and feature flags. Keep conditions centralized and readable; avoid scattering many tiny #if blocks through logic that could instead vary at runtime.
#include <stdio.h>
/* feature switches coming from the compiler command line, for example -DENABLE_VERBOSE=1 */
#ifndef ENABLE_VERBOSE
#define ENABLE_VERBOSE 0
#endif
int main(void) {
#if ENABLE_VERBOSE
printf("Verbose mode active\n");
#endif
#ifdef _WIN32
printf("Windows specific setup\n");
#elif defined(__unix__)
printf("POSIX specific setup\n");
#else
printf("Generic setup\n");
#endif
return 0;
}
Defining symbols at compile time
Pass symbols from your build system to avoid hard coding. With gcc you can use -DNAME=value. This keeps source clean and lets you toggle behavior per build target.
/* compile: gcc -DENABLE_VERBOSE=1 -o app app.c */
#if HAVE_CLOCK_GETTIME over negative ones such as #ifndef NO_TIMERS. Positive tests document what you need rather than what you lack.
Protecting headers and improving modularity
Header files declare interfaces that multiple translation units include. To avoid multiple definition problems and recursive inclusion loops, wrap each header with a unique guard macro. Place declarations in headers and definitions in .c files to keep compile times and dependencies under control.
/* file: mathx.h */
#ifndef MATHX_H_INCLUDED
#define MATHX_H_INCLUDED
#include <stddef.h>
double mean(const double *xs, size_t n);
#endif /* MATHX_H_INCLUDED */
/* file: mathx.c */
#include "mathx.h"
double mean(const double *xs, size_t n) {
double s = 0.0;
for (size_t i = 0; i < n; ++i) s += xs[i];
return n ? s / (double)n : 0.0;
}
/* file: main.c */
#include <stdio.h>
#include "mathx.h"
int main(void) {
double xs[] = {1,2,3};
printf("%.2f\n", mean(xs, 3));
return 0;
}
Avoiding relative tangles
Keep public headers in an include directory and compile with an include path, for example -Iinclude. Use quotes "file.h" for project headers and angle brackets <...> for system headers to document intent.
static inline and understand the implications. Otherwise you will create multiple external definitions at link time.
Understanding the compilation stages
Build tools drive the same three stages even when wrapped by an IDE. Knowing how to invoke them directly makes diagnosing errors much easier. The following workflow uses gcc as an example; the same ideas apply to other toolchains.
| Stage | What happens | Example command |
| Preprocess | Expanding macros, removing comments, inserting headers | gcc -E file.c -Iinclude -DMODE=1 > file.i |
| Compile | Translating preprocessed C into object code | gcc -c file.i -o file.o |
| Assemble | Converting assembly to machine code (often merged with compile) | gcc -S file.c -o file.s |
| Link | Resolving external symbols and producing an executable or library | gcc file.o util.o -lm -o app |
# Build a small program step by step
gcc -E main.c > main.i
gcc -c main.i -o main.o
gcc -c mathx.c -o mathx.o
gcc main.o mathx.o -o app
Diagnosing failures
If errors mention macros or included lines, inspect the preprocessed .i file. If the compiler reports a type mismatch, examine declarations across headers. If the linker reports an undefined reference, check that you compiled every needed .c file and linked the right libraries in the correct order.
gcc main.o -lm. Reversing this may fail to resolve symbols.
The C Standard Library
The Standard Library is a collection of headers and linked implementations that the compiler and linker know how to find. You include its interfaces with directives such as #include <stdio.h>, which the preprocessor expands into declarations. Later, the linker resolves the corresponding symbols by linking in the system libraries, either by default or when you add options such as -lm for math.
| Header | Purpose | Notes |
<stdio.h> | File and stream I/O | Functions like printf(), fopen() |
<stdlib.h> | Memory, conversions, utilities | malloc(), strtol(), qsort() |
<string.h> | Byte and string utilities | memcpy(), strncpy() |
<errno.h> | Error reporting | errno and error codes |
<math.h> | Math functions | May require -lm when linking |
<stdint.h> | Fixed width integer types | uint32_t, int64_t |
<assert.h> | Assertions | Disabled when NDEBUG is defined |
/* compile and link; math sometimes requires -lm */
gcc main.c -o main -lm
Including headers and enabling feature macros
Some library features require defining feature test macros before including headers. This lets you opt into newer interfaces while preserving compatibility. Place the macro at the top of a translation unit or define it in your build system.
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <string.h>
/* now functions like getline() may be available on your platform */
sqrt, check your link line and add the appropriate library switch.
By recognizing that headers participate in preprocessing and that library symbols are resolved during linking, you can place the Standard Library naturally within the same flow that governs your own modules. Good preprocessing habits and a clear build pipeline make library use predictable and portable.