C Preprocessor and Macros

The C preprocessor is a step that runs before the compiler translates your program. It handles instructions that begin with #, such as including header files, replacing macros, and choosing which code should be compiled.

Preprocessor commands are not normal C statements. They are processed as text-based instructions before the compiler checks types and syntax in the final expanded code.

Common Preprocessor Directives

A preprocessor directive starts with # at the beginning of a logical line. You have already used #include to bring in standard library declarations.

Directive Purpose
#include Copies declarations from a header file into the current file.
#define Creates a macro name that the preprocessor replaces before compiling.
#if, #ifdef, #ifndef Includes or excludes code based on preprocessor conditions.
#endif Ends a conditional preprocessor block.

Object-like Macros

An object-like macro is a name that is replaced with a value or text. It is often used for constants that should be easy to change in one place.

#include <stdio.h>

#define MAX_USERS 3
#define APP_NAME "Course Tracker"

int main(void)
{
    printf("%s\n", APP_NAME);
    printf("Maximum users: %d\n", MAX_USERS);

    return 0;
}

Output:

Course Tracker
Maximum users: 3

Before compilation, the preprocessor replaces APP_NAME with "Course Tracker" and MAX_USERS with 3. By convention, macro names are usually written in uppercase so they stand out from variables.

Function-like Macros

A function-like macro accepts arguments and expands into code. It can look like a function call, but it is still text substitution. For simple expressions, wrap each argument and the whole replacement expression in parentheses.

#include <stdio.h>

#define SQUARE(n) ((n) * (n))
#define BIGGER(a, b) ((a) > (b) ? (a) : (b))

int main(void)
{
    int x = 4;
    int y = 7;

    printf("square: %d\n", SQUARE(x + 1));
    printf("bigger: %d\n", BIGGER(x, y));

    return 0;
}

Output:

square: 25
bigger: 7

The parentheses in SQUARE are important. Without them, SQUARE(x + 1) could expand in a way that changes the order of operations.

Macro Pitfalls

Macros are powerful, but they do not behave exactly like functions. A macro argument may be used more than once after expansion, so avoid passing expressions with side effects such as i++.

Prefer normal functions when you need type checking, complex logic, or debugging support. Use macros for simple constants, compile-time choices, and very small expression replacements.

Conditional Compilation

Conditional compilation lets you include code only when a preprocessor condition is true. This is useful for debug output, platform-specific code, and optional features.

#include <stdio.h>

#define ENABLE_LOG 1

#if ENABLE_LOG
#define LOG(message) printf("LOG: %s\n", message)
#else
#define LOG(message) ((void) 0)
#endif

int main(void)
{
    printf("Program started\n");
    LOG("loading settings");
    printf("Program finished\n");

    return 0;
}

Output:

Program started
LOG: loading settings
Program finished

Because ENABLE_LOG is 1, the first version of LOG is used. If you changed it to 0, the logging macro would expand to ((void) 0), which does nothing.

Header Guards

Large C programs often split declarations into header files. A header guard prevents the same header content from being included more than once in a single source file.

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);

#endif

This pattern means: if MATH_UTILS_H has not been defined yet, define it and include the declarations. If the header is included again later, the declarations inside the guard are skipped.

Best Practices

  • Use uppercase names for macros, such as BUFFER_SIZE.
  • Put parentheses around macro arguments in expression macros.
  • Avoid side effects inside macro arguments, such as count++.
  • Use const variables or functions when runtime type checking matters.
  • Use conditional compilation for code that should be included or removed before compiling.

The key idea is that the preprocessor prepares your source text before C compilation begins, so macros are useful but should stay simple and predictable.