Module 8.1

Preprocessor Directives in C

Master the C preprocessor, a powerful tool that transforms your source code before compilation. Learn to define macros for constants and code generation, use conditional compilation for platform-specific code, manage file inclusion, and leverage predefined macros for debugging and version control.

40 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Macro definition and expansion
  • Function-like macros with parameters
  • Conditional compilation techniques
  • The #include directive in depth
  • Predefined macros for debugging
Contents
01

Introduction to the C Preprocessor

The C preprocessor is a text processing tool that runs before the actual compilation begins. It handles directives (lines starting with #) to perform text substitution, file inclusion, and conditional compilation. Understanding the preprocessor is essential for writing portable, maintainable, and efficient C code.

What is the Preprocessor?

When you compile a C program, it goes through several stages. The preprocessor is the first stage, transforming your source code before the compiler ever sees it. All preprocessor directives start with a # symbol and are processed line by line.

Compilation Stages
Source Code
.c file
Preprocessor
#directives
Compiler
Translation
Linker
Combine
Executable
.exe / a.out

Common Preprocessor Directives

Directive Purpose Example
#define Define a macro (constant or function-like) #define PI 3.14159
#include Include contents of another file #include <stdio.h>
#ifdef / #ifndef Conditional compilation based on macro existence #ifdef DEBUG
#if / #elif / #else Conditional compilation with expressions #if VERSION >= 2
#endif End conditional block #endif
#undef Undefine a macro #undef MAX
#pragma Compiler-specific instructions #pragma once
#error Generate a compilation error #error "Not supported"

Viewing Preprocessor Output

You can see what the preprocessor produces using the -E flag with gcc:

# Show preprocessor output (does not compile)
gcc -E program.c -o program.i

# Or display directly to terminal
gcc -E program.c | less
Key Insight

The preprocessor only does text substitution. It does not understand C syntax, types, or scope. This is both powerful (works anywhere) and dangerous (no type checking). Always be careful with macro side effects!

Practice Questions

Task: Which of these lines are preprocessor directives?

int main() {
#include <stdio.h>
    int x = 10;
#define MAX 100
    printf("Hello");
#ifdef DEBUG
    return 0;
}
Show Solution

Preprocessor directives (lines starting with #):

  • #include <stdio.h> - File inclusion
  • #define MAX 100 - Macro definition
  • #ifdef DEBUG - Conditional compilation

Note: The placement of #include inside main() is unusual and bad practice, but technically valid for the preprocessor (it just inserts text).

Task: What does the following code become after preprocessing?

#define VALUE 42
#define DOUBLE(x) x * 2

int result = DOUBLE(VALUE);
Show Solution
// After preprocessing:
int result = 42 * 2;

First, DOUBLE(VALUE) expands to VALUE * 2, then VALUE expands to 42.

02

Macro Definition and Expansion

Macros are the most powerful feature of the C preprocessor. They allow you to define constants, create inline code substitutions, and build code generation tools. However, they must be used carefully to avoid subtle bugs that can be hard to debug.

Object-like Macros (Constants)

The simplest macros define constants that are replaced throughout your code:

#define PI 3.14159265359
#define MAX_BUFFER_SIZE 1024
#define APP_NAME "My Application"
#define NEWLINE '\n'

// Usage
double circumference = 2 * PI * radius;
char buffer[MAX_BUFFER_SIZE];
printf("Welcome to %s%c", APP_NAME, NEWLINE);
Directive

#define

#define creates a macro that tells the preprocessor to replace all occurrences of the macro name with its replacement text. The replacement happens before compilation, as simple text substitution.

Convention: Macro names are typically written in ALL_CAPS to distinguish them from variables and functions.

Function-like Macros

Macros can take parameters, making them act like inline functions:

// Simple function-like macros
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#define ABS(x) ((x) < 0 ? -(x) : (x))

// Usage
int result = SQUARE(5);      // Expands to: ((5) * (5))
int bigger = MAX(10, 20);    // Expands to: ((10) > (20) ? (10) : (20))
int absolute = ABS(-42);     // Expands to: ((-42) < 0 ? -(-42) : (-42))

Why All the Parentheses?

Parentheses are critical in macros to prevent operator precedence bugs:

// BAD: Missing parentheses
#define SQUARE_BAD(x) x * x

int a = SQUARE_BAD(3 + 2);
// Expands to: 3 + 2 * 3 + 2
// Evaluates as: 3 + 6 + 2 = 11 (wrong!)

// GOOD: Proper parentheses
#define SQUARE_GOOD(x) ((x) * (x))

int b = SQUARE_GOOD(3 + 2);
// Expands to: ((3 + 2) * (3 + 2))
// Evaluates as: 5 * 5 = 25 (correct!)

Multi-line Macros

Use backslash \ to continue a macro on the next line:

// Multi-line macro with do-while(0) pattern
#define SWAP(a, b, type) do { \
    type temp = a;            \
    a = b;                    \
    b = temp;                 \
} while(0)

// Usage (note: no semicolon issues)
int x = 5, y = 10;
SWAP(x, y, int);
printf("x=%d, y=%d\n", x, y);  // x=10, y=5

Stringification and Token Pasting

Special operators let you manipulate macro arguments as text:

// # - Stringification (convert to string literal)
#define STRINGIFY(x) #x
#define PRINT_VAR(var) printf(#var " = %d\n", var)

int count = 42;
PRINT_VAR(count);  // Expands to: printf("count" " = %d\n", count);
                   // Prints: count = 42

// ## - Token pasting (concatenate tokens)
#define MAKE_FUNC(name) void func_##name() { printf(#name "\n"); }

MAKE_FUNC(hello)   // Creates: void func_hello() { printf("hello\n"); }
MAKE_FUNC(world)   // Creates: void func_world() { printf("world\n"); }

Macro Pitfalls: Side Effects

Danger: Double Evaluation

Macro arguments are substituted literally, so expressions with side effects can be evaluated multiple times:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int i = 5;
int result = MAX(i++, 3);  // Expands to: ((i++) > (3) ? (i++) : (3))
// i gets incremented TWICE if i > 3!
// After: i = 7, result = 6 (not what you expected!)

Undefining Macros

#define DEBUG 1

// ... some code using DEBUG ...

#undef DEBUG  // Remove the macro definition

// DEBUG is no longer defined here
#ifdef DEBUG
    printf("This won't compile\n");
#endif
Practice Questions

Task: Define a macro ARRAY_SIZE that calculates the number of elements in an array.

Show Solution
#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))

// Usage
int numbers[] = {1, 2, 3, 4, 5};
printf("Array has %zu elements\n", ARRAY_SIZE(numbers));  // 5

char name[] = "Hello";
printf("String array size: %zu\n", ARRAY_SIZE(name));  // 6 (includes '\0')

Task: Create a DEBUG_PRINT macro that prints the variable name and value, but only when DEBUG is defined.

Show Solution
#define DEBUG  // Comment out to disable

#ifdef DEBUG
    #define DEBUG_PRINT(var, fmt) \
        printf("[DEBUG] %s = " fmt "\n", #var, var)
#else
    #define DEBUG_PRINT(var, fmt) // Empty - does nothing
#endif

// Usage
int count = 42;
double pi = 3.14159;
char *name = "Alice";

DEBUG_PRINT(count, "%d");    // [DEBUG] count = 42
DEBUG_PRINT(pi, "%.2f");     // [DEBUG] pi = 3.14
DEBUG_PRINT(name, "%s");     // [DEBUG] name = Alice

Task: Create MIN/MAX macros that evaluate arguments only once using GCC statement expressions.

Show Solution
// GCC extension: statement expressions ({...})
// Evaluates each argument only once!

#define SAFE_MAX(a, b) ({   \
    typeof(a) _a = (a);     \
    typeof(b) _b = (b);     \
    _a > _b ? _a : _b;      \
})

#define SAFE_MIN(a, b) ({   \
    typeof(a) _a = (a);     \
    typeof(b) _b = (b);     \
    _a < _b ? _a : _b;      \
})

// Now safe to use with side effects
int i = 5;
int result = SAFE_MAX(i++, 3);
// i is incremented only once!
// After: i = 6, result = 5

// Note: This is a GCC extension, not standard C
03

Conditional Compilation

Conditional compilation allows you to include or exclude code based on compile-time conditions. This is essential for platform-specific code, debug builds, feature toggles, and preventing header files from being included multiple times (include guards).

#ifdef and #ifndef

Check if a macro is defined or not defined:

// #ifdef - if defined
#ifdef DEBUG
    printf("Debug mode enabled\n");
    printf("Variable x = %d\n", x);
#endif

// #ifndef - if NOT defined
#ifndef RELEASE
    // This code only compiles in non-release builds
    runTests();
#endif

// With #else
#ifdef _WIN32
    #include <windows.h>
    #define CLEAR_SCREEN system("cls")
#else
    #include <unistd.h>
    #define CLEAR_SCREEN system("clear")
#endif

#if, #elif, #else

Use expressions and multiple conditions:

// Check macro values
#define VERSION 3

#if VERSION == 1
    printf("Version 1.0\n");
#elif VERSION == 2
    printf("Version 2.0\n");
#elif VERSION >= 3
    printf("Version 3.0 or later\n");
#else
    printf("Unknown version\n");
#endif

// Combine conditions with && and ||
#if defined(DEBUG) && defined(VERBOSE)
    #define LOG(msg) printf("[DEBUG] %s\n", msg)
#elif defined(DEBUG)
    #define LOG(msg) printf("%s\n", msg)
#else
    #define LOG(msg)  // Empty - no logging
#endif

Include Guards

The most common use of conditional compilation is preventing double inclusion of headers:

// myheader.h
#ifndef MYHEADER_H    // If MYHEADER_H is not defined...
#define MYHEADER_H    // Define it

// Header contents go here
typedef struct {
    int x, y;
} Point;

void doSomething(void);

#endif  // End of include guard

// Alternative: #pragma once (non-standard but widely supported)
#pragma once

typedef struct {
    int x, y;
} Point;

void doSomething(void);

Platform-Specific Code

// Detect operating system
#if defined(_WIN32) || defined(_WIN64)
    #define PLATFORM "Windows"
    #define PATH_SEPARATOR '\\'
#elif defined(__APPLE__) && defined(__MACH__)
    #define PLATFORM "macOS"
    #define PATH_SEPARATOR '/'
#elif defined(__linux__)
    #define PLATFORM "Linux"
    #define PATH_SEPARATOR '/'
#else
    #define PLATFORM "Unknown"
    #define PATH_SEPARATOR '/'
#endif

printf("Running on %s\n", PLATFORM);

Feature Toggles

// config.h - Feature flags
#define FEATURE_LOGGING 1
#define FEATURE_ENCRYPTION 0
#define FEATURE_ANALYTICS 1

// In code
#if FEATURE_LOGGING
    #include "logger.h"
    #define LOG(msg) log_message(msg)
#else
    #define LOG(msg)
#endif

#if FEATURE_ENCRYPTION
    #include "crypto.h"
    void saveData(const char *data) {
        char *encrypted = encrypt(data);
        writeFile(encrypted);
        free(encrypted);
    }
#else
    void saveData(const char *data) {
        writeFile(data);
    }
#endif

#error and #warning

// Generate compile-time errors
#ifndef CONFIG_FILE
    #error "CONFIG_FILE must be defined"
#endif

#if BUFFER_SIZE < 256
    #error "BUFFER_SIZE must be at least 256"
#endif

// Generate compile-time warnings (GCC extension)
#if VERSION < 2
    #warning "Version 1.x is deprecated, please upgrade"
#endif
Define Macros from Command Line

You can define macros when compiling without changing source code:
gcc -DDEBUG program.c - defines DEBUG
gcc -DVERSION=3 program.c - defines VERSION as 3

Practice Questions

Task: Write proper include guards for a header file called "utils.h".

Show Solution
// utils.h
#ifndef UTILS_H
#define UTILS_H

// Function declarations
int max(int a, int b);
int min(int a, int b);
void swap(int *a, int *b);

// Type definitions
typedef unsigned int uint;

#endif  // UTILS_H

Task: Create a set of macros that provide ASSERT, LOG, and TRACE functionality only in debug builds.

Show Solution
// debug.h
#ifndef DEBUG_H
#define DEBUG_H

#include <stdio.h>
#include <stdlib.h>

#ifdef DEBUG
    #define ASSERT(cond) do { \
        if (!(cond)) { \
            fprintf(stderr, "Assertion failed: %s\n", #cond); \
            fprintf(stderr, "  File: %s, Line: %d\n", __FILE__, __LINE__); \
            abort(); \
        } \
    } while(0)
    
    #define LOG(fmt, ...) \
        printf("[LOG] " fmt "\n", ##__VA_ARGS__)
    
    #define TRACE() \
        printf("[TRACE] %s() at %s:%d\n", __func__, __FILE__, __LINE__)
#else
    #define ASSERT(cond)       // Empty
    #define LOG(fmt, ...)      // Empty
    #define TRACE()            // Empty
#endif

#endif  // DEBUG_H

// Usage:
// gcc -DDEBUG program.c    (debug build)
// gcc program.c            (release build - macros do nothing)

Task: Create a portable SLEEP(ms) macro that works on Windows, Linux, and macOS.

Show Solution
// portable_sleep.h
#ifndef PORTABLE_SLEEP_H
#define PORTABLE_SLEEP_H

#if defined(_WIN32) || defined(_WIN64)
    #include <windows.h>
    #define SLEEP(ms) Sleep(ms)
#elif defined(__APPLE__) || defined(__linux__) || defined(__unix__)
    #include <unistd.h>
    #define SLEEP(ms) usleep((ms) * 1000)
#else
    #error "Unsupported platform for SLEEP macro"
#endif

#endif  // PORTABLE_SLEEP_H

// Usage
#include "portable_sleep.h"

int main() {
    printf("Waiting 2 seconds...\n");
    SLEEP(2000);  // Sleep for 2000 milliseconds
    printf("Done!\n");
    return 0;
}
04

The #include Directive

The #include directive is fundamental to C programming. It tells the preprocessor to insert the contents of another file at that point in your code. Understanding how include works and the difference between angle brackets and quotes is essential.

Two Forms of #include

#include <header.h>

Angle brackets for system/standard headers

  • Searches system include directories
  • Used for standard library headers
  • Compiler knows where to find these
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "header.h"

Quotes for project/local headers

  • Searches current directory first
  • Then searches system directories
  • Used for your own header files
#include "myheader.h"
#include "utils/helpers.h"
#include "../common/types.h"

How #include Works

The preprocessor literally copies and pastes the file contents:

// math_utils.h
int add(int a, int b);
int subtract(int a, int b);

// main.c
#include "math_utils.h"

int main() {
    return add(5, 3);
}

// After preprocessing, main.c becomes:
int add(int a, int b);
int subtract(int a, int b);

int main() {
    return add(5, 3);
}

Include Search Paths

You can add custom directories to the include search path:

# Add include directory with -I flag
gcc -I./include -I../common main.c -o program

# Now these work:
#include "myheader.h"      # Searches ./include and ../common
#include <myheader.h>      # Also searches these paths

Standard Library Headers

Header Purpose Key Functions/Macros
<stdio.h> Standard I/O printf, scanf, fopen, fclose
<stdlib.h> General utilities malloc, free, atoi, exit, rand
<string.h> String handling strlen, strcpy, strcmp, memcpy
<math.h> Math functions sqrt, sin, cos, pow, log
<ctype.h> Character handling isalpha, isdigit, toupper, tolower
<time.h> Date and time time, clock, strftime, difftime
<stdbool.h> Boolean type (C99) bool, true, false
<stdint.h> Fixed-width integers (C99) int32_t, uint8_t, INT_MAX

Nested Includes and Dependencies

// types.h
#ifndef TYPES_H
#define TYPES_H
typedef struct { int x, y; } Point;
#endif

// graphics.h
#ifndef GRAPHICS_H
#define GRAPHICS_H

#include "types.h"  // Needs Point type

void drawPoint(Point p);
void drawLine(Point a, Point b);

#endif

// main.c
#include "types.h"     // Included first
#include "graphics.h"  // Also includes types.h, but guards prevent duplicate

// Without include guards, Point would be defined twice = error!
Best Practices for #include
  • Always use include guards in your headers
  • Include what you use directly, do not rely on transitive includes
  • Put system headers before local headers
  • Order includes alphabetically within each group
  • Do not include .c files, only .h files
Practice Questions

Task: Which include style (angle brackets or quotes) should be used for each?

  • stdio.h
  • myproject.h (your own header)
  • math.h
  • ../utils/helper.h
Show Solution
#include <stdio.h>         // System header - angle brackets
#include "myproject.h"      // Local header - quotes
#include <math.h>           // System header - angle brackets
#include "../utils/helper.h" // Local header with path - quotes

Task: This code gives "redefinition" errors. Fix it:

// point.h
typedef struct { int x, y; } Point;

// rect.h
#include "point.h"
typedef struct { Point topLeft, bottomRight; } Rect;

// main.c
#include "point.h"
#include "rect.h"
// Error: Point redefined!
Show Solution
// point.h - Add include guards
#ifndef POINT_H
#define POINT_H
typedef struct { int x, y; } Point;
#endif

// rect.h - Add include guards
#ifndef RECT_H
#define RECT_H
#include "point.h"
typedef struct { Point topLeft, bottomRight; } Rect;
#endif

// main.c - Now works correctly
#include "point.h"  // Defines Point
#include "rect.h"   // Includes point.h again, but guards skip it
05

Predefined Macros

C provides several predefined macros that are automatically available in every program. These are invaluable for debugging, logging, conditional compilation, and creating informative error messages. They give you information about the compilation environment.

Standard Predefined Macros

Macro Description Example Value
__FILE__ Current source file name (string) "main.c"
__LINE__ Current line number (integer) 42
__func__ Current function name (C99, string) "calculateSum"
__DATE__ Compilation date (string) "Feb 1 2026"
__TIME__ Compilation time (string) "14:30:00"
__STDC__ 1 if compiler conforms to C standard 1
__STDC_VERSION__ C standard version (C99+) 201710L (C17)

Using Predefined Macros for Debugging

#include <stdio.h>

// Debug logging with file and line info
#define DEBUG_LOG(msg) \
    printf("[%s:%d] %s\n", __FILE__, __LINE__, msg)

// Assert with detailed error info
#define ASSERT(condition) do { \
    if (!(condition)) { \
        fprintf(stderr, "Assertion failed: %s\n", #condition); \
        fprintf(stderr, "  Function: %s\n", __func__); \
        fprintf(stderr, "  File: %s\n", __FILE__); \
        fprintf(stderr, "  Line: %d\n", __LINE__); \
        abort(); \
    } \
} while(0)

void processData(int *data, int count) {
    ASSERT(data != NULL);
    ASSERT(count > 0);
    
    DEBUG_LOG("Processing started");
    // ... process data ...
    DEBUG_LOG("Processing complete");
}

int main() {
    processData(NULL, 5);  // This will trigger assertion
    return 0;
}

Build Information

#include <stdio.h>

void printBuildInfo() {
    printf("=== Build Information ===\n");
    printf("Compiled on: %s at %s\n", __DATE__, __TIME__);
    printf("Source file: %s\n", __FILE__);
    
#ifdef __STDC_VERSION__
    printf("C Standard: ");
    #if __STDC_VERSION__ >= 201710L
        printf("C17\n");
    #elif __STDC_VERSION__ >= 201112L
        printf("C11\n");
    #elif __STDC_VERSION__ >= 199901L
        printf("C99\n");
    #else
        printf("C89/C90\n");
    #endif
#endif

#ifdef __GNUC__
    printf("Compiler: GCC %d.%d.%d\n", 
           __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
#elif defined(_MSC_VER)
    printf("Compiler: MSVC %d\n", _MSC_VER);
#elif defined(__clang__)
    printf("Compiler: Clang %d.%d\n", 
           __clang_major__, __clang_minor__);
#endif
}

Compiler-Specific Macros

// GCC-specific macros
#ifdef __GNUC__
    #define UNUSED __attribute__((unused))
    #define DEPRECATED __attribute__((deprecated))
    #define PRINTF_FORMAT(a, b) __attribute__((format(printf, a, b)))
#else
    #define UNUSED
    #define DEPRECATED
    #define PRINTF_FORMAT(a, b)
#endif

// Usage
UNUSED static void helperFunc() { }

DEPRECATED void oldFunction() {
    // Compiler warns when this is used
}

PRINTF_FORMAT(1, 2)
void myPrintf(const char *fmt, ...) {
    // Compiler checks format string
}

Creating a Comprehensive Logger

// logger.h
#ifndef LOGGER_H
#define LOGGER_H

#include <stdio.h>
#include <time.h>

typedef enum { LOG_DEBUG, LOG_INFO, LOG_WARN, LOG_ERROR } LogLevel;

#define LOG(level, fmt, ...) do { \
    time_t now = time(NULL); \
    struct tm *t = localtime(&now); \
    char timeStr[20]; \
    strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", t); \
    const char *levelStr[] = {"DEBUG", "INFO", "WARN", "ERROR"}; \
    fprintf(stderr, "[%s] [%s] [%s:%d] " fmt "\n", \
            timeStr, levelStr[level], __FILE__, __LINE__, ##__VA_ARGS__); \
} while(0)

#define LOG_DEBUG(fmt, ...) LOG(LOG_DEBUG, fmt, ##__VA_ARGS__)
#define LOG_INFO(fmt, ...)  LOG(LOG_INFO, fmt, ##__VA_ARGS__)
#define LOG_WARN(fmt, ...)  LOG(LOG_WARN, fmt, ##__VA_ARGS__)
#define LOG_ERROR(fmt, ...) LOG(LOG_ERROR, fmt, ##__VA_ARGS__)

#endif

// Usage
LOG_INFO("Server started on port %d", 8080);
LOG_WARN("Connection timeout after %d seconds", 30);
LOG_ERROR("Failed to open file: %s", filename);
The ##__VA_ARGS__ Trick

In macros with variable arguments (...), ##__VA_ARGS__ removes the trailing comma if no arguments are passed. This is a GCC extension that prevents syntax errors when calling LOG_INFO("message") without extra arguments.

Practice Questions

Task: Write a function that prints "Built on [date] at [time]" using predefined macros.

Show Solution
#include <stdio.h>

void printBuildTimestamp() {
    printf("Built on %s at %s\n", __DATE__, __TIME__);
}

int main() {
    printBuildTimestamp();
    // Output: Built on Feb  1 2026 at 14:30:00
    return 0;
}

Task: Create TRACE_ENTER and TRACE_EXIT macros that print function entry and exit.

Show Solution
#include <stdio.h>

#ifdef TRACE_ENABLED
    #define TRACE_ENTER() \
        printf("--> Entering %s() [%s:%d]\n", __func__, __FILE__, __LINE__)
    #define TRACE_EXIT() \
        printf("<-- Exiting %s() [%s:%d]\n", __func__, __FILE__, __LINE__)
#else
    #define TRACE_ENTER()
    #define TRACE_EXIT()
#endif

void innerFunction() {
    TRACE_ENTER();
    printf("Doing work...\n");
    TRACE_EXIT();
}

void outerFunction() {
    TRACE_ENTER();
    innerFunction();
    TRACE_EXIT();
}

int main() {
    TRACE_ENTER();
    outerFunction();
    TRACE_EXIT();
    return 0;
}

// Compile with: gcc -DTRACE_ENABLED program.c

Task: Create a STATIC_ASSERT macro that generates a compile-time error if a condition is false.

Show Solution
// For C11 and later, use _Static_assert
#if __STDC_VERSION__ >= 201112L
    #define STATIC_ASSERT(cond, msg) _Static_assert(cond, msg)
#else
    // Pre-C11 fallback: creates array with negative size if false
    #define STATIC_ASSERT(cond, msg) \
        typedef char static_assertion_##__LINE__[(cond) ? 1 : -1]
#endif

// Usage examples
STATIC_ASSERT(sizeof(int) == 4, "int must be 4 bytes");
STATIC_ASSERT(sizeof(void*) >= 4, "Pointer must be at least 4 bytes");

typedef struct {
    int id;
    char name[32];
    float value;
} Record;

// Ensure struct fits in expected size
STATIC_ASSERT(sizeof(Record) <= 64, "Record struct too large");

int main() {
    // If any assertion fails, compilation stops with an error
    return 0;
}

Key Takeaways

Preprocessor First

The preprocessor runs before compilation, doing text substitution on # directives

#define Macros

Create constants and function-like macros, always use parentheses for safety

Conditional Compilation

#ifdef, #ifndef, #if for platform code, debug builds, and feature toggles

#include Properly

Angle brackets for system headers, quotes for local headers, always use include guards

Predefined Macros

Use __FILE__, __LINE__, __func__ for debugging and informative error messages

Macro Pitfalls

Watch for double evaluation and side effects, macros have no type safety

Knowledge Check

Quick Quiz

Test what you have learned about C preprocessor directives

1 When does the C preprocessor run?
2 What is the purpose of parentheses in #define SQUARE(x) ((x) * (x))?
3 What do include guards prevent?
4 What does __LINE__ expand to?
5 What is the difference between #include <file.h> and #include "file.h"?
6 What is a major pitfall of function-like macros?
Answer all questions to check your score