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.

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.
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:
#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.
The preprocessor can filter code blocks based on conditions. The #ifdef pattern is used for conditional compilation, while #pragma once simplifies include guards:
#ifdef DEBUG
printf("mode debug\n");
#endifIf DEBUG is defined during compilation, the block above gets compiled. Enable it with the -D flag:
gcc -DDEBUG program.c -o programThe 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.
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:
#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.
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.
A Makefile structures the build as rules in the format of target, dependencies, and commands. Command lines must start with a tab character:
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: cleanThe 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.
Run the whole build with a single command:
make
./appmake 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.
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.
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.
CMake is used in two steps: configure, then build:
cmake -S . -B build
cmake --build build
./build/appThe 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.
Key takeaways:
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.