After studying memory bugs in lesson 14, let’s focus on the stage that runs before the compiler: preprocessing. The preprocessor rewrites your code as text before the compiler sees it. It replaces text, splices files together, and enables or disables code based on conditions. Understanding this step makes headers, constant definitions, and build environments much clearer.
New Terms in This Post
- Preprocessor: The tool that handles directives beginning with
#before compilation - Macro: A rule that replaces one piece of text with another
- Conditional compilation: Using
#if,#ifdef,#ifndef, etc., to include or exclude code - Include guard: A pattern that prevents a header from being included multiple times
Key Ideas
Study Notes
- Time required: 45–60 minutes
- Prerequisites: familiarity with headers, function declarations, and basic build flow
- Goal: recognize preprocessor directives and write practical macros and guards
Think of preprocessing as three major roles. Remember that it does not execute code— it edits text before the compiler reads it.
- File merging:
#includeinlines headers so the compiler can see the declarations it needs. - Constant/function substitution:
#definereplaces repeated numbers or patterns with names. - Conditional branching: Choose code per platform or build mode.
We will cover:
#includeand guard structure- Simple constant macros plus safe arithmetic macros with parentheses
- Function-like macros and the pitfalls of argument evaluation
- Conditional compilation for debug logging
- Inspecting preprocessor output with
gcc -Eorclang -E
Code Walkthrough
#include and Guard Pattern
Including a header twice can trigger duplicate declarations, so guard it. An include guard exists to stop the same header from being processed again in one compilation.
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
#endif
#ifndef checks whether a symbol is undefined. After the first include, #define marks it as defined. Later includes skip the guarded block. Think of it as a lock preventing repeated declarations. #pragma once is a shorter alternative, but include guards are the traditional, portable approach.
Managing Numbers with Constant Macros
#include <stdio.h>
#define MAX_USERS 100
#define PI 3.1415926535
int main(void) {
printf("Max users: %d\n", MAX_USERS);
printf("PI = %f\n", PI);
return 0;
}
Macros use #define NAME value. The preprocessor literally pastes the replacement text everywhere the name appears. By convention, use uppercase with underscores for macro names. For simple named constants, const variables or enum values are often safer and easier to reason about, but macros remain common for compile-time constants.
Arithmetic Macros and Parentheses
Macros look like functions but are just text replacement, so parentheses matter.
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int main(void) {
printf("SQUARE(3) = %d\n", SQUARE(3));
printf("SQUARE(1 + 2) = %d\n", SQUARE(1 + 2));
return 0;
}
SQUARE(1 + 2) becomes ((1 + 2) * (1 + 2)), which equals 9. Without parentheses you’d get 1 + 2 * 1 + 2, a totally different result. Wrap the whole macro body and each argument in parentheses to keep operator precedence under control.
Beware of Arguments with Side Effects
#include <stdio.h>
#define DOUBLE(x) ((x) + (x))
int main(void) {
int i = 2;
printf("%d\n", DOUBLE(++i));
printf("i = %d\n", i);
return 0;
}
DOUBLE(++i) expands to (++i) + (++i), so i increments twice. A real function would evaluate ++i once, but macros paste the expression wherever it appears. Here, a side effect means an expression changes program state while it is being evaluated. Avoid passing expressions with side effects such as increments or function calls to macros.
Conditional Compilation for Debug Logs
#include <stdio.h>
#define DEBUG 1
#if DEBUG
#define LOG(msg) printf("[DEBUG] %s:%d %s\n", __FILE__, __LINE__, msg)
#else
#define LOG(msg) ((void)0)
#endif
int main(void) {
LOG("start");
// ... actual work ...
LOG("end");
return 0;
}
Set DEBUG to 0 or compile with -DDEBUG=0 to remove logging. #if DEBUG tests whether the value is nonzero, while #ifdef DEBUG simply checks whether the symbol exists. Use similar patterns for platform-specific code (e.g., #if defined(_WIN32)).
Inspecting Preprocessor Output
Curious about what the compiler truly sees? Use -E to stop after preprocessing without compiling the code.
clang -E main.c
The output contains expanded includes and macros. Because the result is large, redirect it to a file such as main.pp.c and inspect that file.
Why It Matters
- Understanding preprocessing makes header structures and build errors easier to decode.
- Macros remove repetition and manage constants, and strict parentheses reduce surprises.
- Conditional compilation toggles platform-specific or debug-only code with minimal effort.
Practice in CodeSandbox
The sandbox below uses CodeSandbox's Universal starter. For C, the key learning loop is still compile and run in the terminal, so recreate the lesson code as a source file and repeat that cycle directly.
💬 댓글
이 글에 대한 의견을 남겨주세요