Learn C Language - Preprocessor & Build Systems
Episode 10 of 24

Learn C Language - Preprocessor & Build Systems

This episode covers the preprocessor: #include, #define, #ifdef, and #pragma, then safe function macros and constants, writing basic Makefiles with rules and targets, and an introduction to CMake for portable cross-platform projects.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

The preprocessor is the first stage of the compilation pipeline you learned in episode 2. Everything that starts with the # sign is processed before the compiler sees the code. Episode 10 covers preprocessor directives thoroughly and how to use them safely.

In the second half, you learn build systems: make with a Makefile for simple projects, and cmake for large projects that must be portable across many platforms. Build systems turn compilation from repeated manual commands into a single automated command.

After this episode, your C projects will have a clean build foundation that anyone can run with a single command.

Preprocessor Directives

#include and #define

The #include directive inserts the contents of a file into the translation unit. Angle brackets are for standard libraries, quotes for local project headers. The #define directive defines macros, often used as constants:

Basic directives
#include <stdio.h>
 
#define VERSI 3
#define NAMA_PROGRAM "tool-konversi"
 
int main(void) {
    printf("%s versi %d\n", NAMA_PROGRAM, VERSI);
    return 0;
}

The directive #define VERSI 3 replaces every occurrence of VERSI with 3 before compilation. Unlike const constants, macros are pure text substitution without a type and without a memory location.

#ifdef and #pragma

The preprocessor can filter code blocks based on conditions. The #ifdef pattern is used for conditional compilation, while #pragma once simplifies include guards:

Conditional compilation
#ifdef DEBUG
printf("mode debug\n");
#endif

If DEBUG is defined during compilation, the block above gets compiled. Enable it with the -D flag:

Define a macro from the command line
gcc -DDEBUG program.c -o program

The flag -DDEBUG is equivalent to putting #define DEBUG at the top of the file. This technique is the basis of cross-platform conditional compilation, which we'll cover in episode 20.

Safe Function Macros and Constants

The Trap of Text Substitution

Function macros are dangerous because they substitute raw text. The macro #define KUADRAT(x) x * x will incorrectly evaluate KUADRAT(2 + 3) as 2 + 3 * 2 + 3. The solution is wrapping parameters and results in parentheses:

Safe function macro
#define KUADRAT(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

The pattern #define MAX(a, b) ((a) > (b) ? (a) : (b)) encloses every parameter and the entire expression in parentheses. Even so, a macro evaluated twice can double side effects. For complex logic, a static inline function is safer.

Preferring Constants and Functions

For typed constants, use const or enum rather than #define. For computations, use static inline functions, which are as fast as macros but still respect type rules. Reserve #define for things the preprocessor genuinely needs to know, like include guards and conditional compilation flags.

Basic Makefile

Rules and Targets

A Makefile structures the build as rules in the format of target, dependencies, and commands. Command lines must start with a tab character:

Minimal Makefile
CC = gcc
CFLAGS = -Wall -Wextra -g
 
app: main.c math_utils.c
	$(CC) $(CFLAGS) main.c math_utils.c -o app
 
clean:
	rm -f app
 
.PHONY: clean

The target app depends on two source files; if either changes, make runs its command again. The clean target removes build artifacts, and .PHONY tells make that clean isn't a file name.

Running Make

Run the whole build with a single command:

Build with make
make
./app

make reads the Makefile, checks file modification times, and compiles only what changed. This is the main reason to use a build system: rebuilding a large project from scratch happens once, and small changes only compile the affected files.

Introduction to CMake

Why CMake

A Makefile is written for one platform. CMake is a meta-build system: you write a single CMakeLists.txt, and CMake generates a Makefile, Ninja, or IDE project according to the target platform. This makes projects portable to Linux, macOS, and Windows.

Basic CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(tool_konversi LANGUAGES C)
 
add_executable(app main.c math_utils.c)
target_compile_options(app PRIVATE -Wall -Wextra)

The CMakeLists.txt file above declares an executable named app from two sources and adds warning options. The syntax is simple: add_executable defines the binary, and target_compile_options attaches flags to a specific target.

The CMake Build Flow

CMake is used in two steps: configure, then build:

CMake configure and build
cmake -S . -B build
cmake --build build
./build/app

The command cmake -S . -B build configures the project into the build directory, and cmake --build build compiles it. All generated files are stored in the build folder, keeping the source directory clean — a best practice we'll use again in episode 19.

Tip

Start with make for small projects and switch to CMake once the project has many targets or must compile on many platforms. CMake can generate a Makefile, so both flows complement each other.

Closing

Key takeaways:

  • #include inserts headers, #define defines macros.
  • #ifdef filters code and #pragma once simplifies include guards.
  • Function macros must wrap every parameter in parentheses.
  • Makefiles use rules with targets, dependencies, and commands.
  • CMake generates build files for many platforms from a single CMakeLists.txt.
  • CMake is used in two steps: configure, then build.

In the next episode 11 we will discuss error handling and debugging — error propagation with return codes and errno, diagnostics with assert and logging, debugging with GDB and LLDB using breakpoints, backtraces, and watchpoints, and static analysis tools like clang-tidy, cppcheck, and sanitizers.

Learn C Language - Preprocessor & Build Systems | Learn C Language