Phase 3 · Structured & Modular CModule 12~46 min read

Preprocessor, Headers & Modular Programs

Split programs across translation units using headers, declarations, definitions, macros, conditional compilation, and include guards.

What you'll learn

Real C programs divide declarations from definitions and compile source files independently. Headers describe contracts; source files own implementations; the linker combines them into one program.

By the end, you'll be able to:

  • Explain preprocessing, translation units, object files, and linking
  • Create guarded headers with public declarations
  • Write safer macros and conditional compilation
  • Control whether definitions are visible within or across translation units

Translation units

Preprocessing a source file together with everything it includes produces onetranslation unit. Each translation unit compiles independently to an object file; the linker resolves references between them.

Independent compilation, then linking
main.c

preprocess + compile

main.o

+
calculator.c

preprocess + compile

calculator.o

+
format.c

preprocess + compile

format.o

→ linker →
calculator

executable

DiagnosticTypical cause
Compiler errorInvalid syntax, incompatible types, or missing declaration in one translation unit
Undefined referenceA declaration was used, but no matching definition reached the linker
Multiple definitionMore than one external definition was linked

Headers & source files

A header exposes the interface clients need: function declarations, public types, constants, and carefully chosen macros. A source file includes its own header and provides definitions.

calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H

double add(double left, double right);
double subtract(double left, double right);

#endif
calculator.c
#include "calculator.h"

double add(double left, double right) {
    return left + right;
}

double subtract(double left, double right) {
    return left - right;
}
main.c
#include <stdio.h>
#include "calculator.h"

int main(void) {
    printf("%.1f\n", add(7.0, 5.0));
    printf("%.1f\n", subtract(7.0, 5.0));
    return 0;
}

Key idea

Include a module's own header first in its implementation. If the header is not self-contained or a definition disagrees with its declaration, the compiler reveals it immediately.

Include guards

Headers can be reached through several include paths. An include guard ensures the header's contents are processed only once per translation unit.

  1. #ifndef CALCULATOR_H asks whether the guard is absent.
  2. #define CALCULATOR_H marks the header as included.
  3. #endif closes the guarded region.

Note

#pragma once is widely supported but not standardized by ISO C. Traditional guards remain maximally portable.

Macros

The preprocessor replaces tokens before the compiler sees C expressions. Macros have no type system and can evaluate an argument more than once.

macros.c
#include <stdio.h>

#define ARRAY_COUNT(array) (sizeof(array) / sizeof((array)[0]))
#define MAXIMUM(a, b) ((a) > (b) ? (a) : (b))

int main(void) {
    int values[] = {2, 4, 6, 8};
    printf("%zu\n", ARRAY_COUNT(values));
    printf("%d\n", MAXIMUM(7, 12));

    // Avoid MAXIMUM(i++, j++): a macro argument may be evaluated twice.
    return 0;
}

Parentheses are mandatory discipline

Parenthesize each parameter and the complete replacement expression. Even then, do not pass side-effecting expressions to a macro that mentions a parameter more than once.

Prefer typed functions for behavior. Macros remain appropriate for conditional compilation, generic compile-time patterns, and operations such as array counts that require the operand to remain an array expression.

Conditional compilation

#if, #ifdef, and related directives include source selectively. Build flags can enable diagnostics, platform adapters, optional features, and version strings.

conditional.c
#include <stdio.h>

#ifndef APP_VERSION
#define APP_VERSION "development"
#endif

void log_debug(const char *message) {
#ifdef DEBUG
    fprintf(stderr, "[debug] %s\n", message);
#else
    (void) message;
#endif
}

int main(void) {
    log_debug("starting");
    printf("Version: %s\n", APP_VERSION);
    return 0;
}
Terminal
gcc -DDEBUG -DAPP_VERSION="1.0" conditional.c -o app

Linkage

Linkage determines whether declarations in different scopes or translation units refer to the same entity. File-scope static gives internal linkage and keeps helpers private.

counter.c
// counter.c
static int count = 0;      // internal linkage: private to this translation unit

static void increment(void) {
    count++;
}

int next_count(void) {     // external linkage: public definition
    increment();
    return count;
}
FormRole
static at file scopePrivate definition in this translation unit
extern declarationRefer to an external definition elsewhere
ordinary function definitionExternal linkage by default

Build a multi-file project

The -c option compiles without linking. Only changed translation units need recompilation, which becomes important as projects grow.

Terminal
gcc -std=c17 -Wall -Wextra -Wpedantic -c calculator.c
gcc -std=c17 -Wall -Wextra -Wpedantic -c main.c
gcc calculator.o main.o -o calculator
./calculator

Tip

Keep public headers small, avoid circular includes, forward-declare opaque types when possible, and never place ordinary external variable or function definitions in a header.

Recap & quick check

Key takeaways

  • A preprocessed source file forms one translation unit, compiled independently to object code.
  • Headers publish declarations; source files provide definitions.
  • Include guards prevent repeated header contents within one translation unit.
  • Macros are token substitution, so parenthesization and side effects require care.
  • File-scope static gives internal linkage and keeps implementation details private.

Quick check

1. What does the linker combine?

2. What belongs in a public header?

3. Why can MAX(i++, j++) be dangerous?

4. What does static mean on a file-scope function?

Your programs can now scale across files. Next up: Module 13 — File Handling & Persistent Data, where information survives after the process exits.