Introduction

Related material. The GATE 2027 CS syllabus does not name the preprocessor separately. It is included because it is part of the syllabus item "Programming in C": C programs in questions use #define and #include.

Before a C file is compiled, it passes through the preprocessor, which works on tokens rather than on C expressions or types. It removes comments, joins lines ending in a backslash, copies in header files (#include), replaces macro names by their definitions (#define), and keeps or drops blocks of code (#if, #ifdef). The compiler proper sees the preprocessing result rather than active macro invocations. A name suppressed from recursive expansion may remain as an ordinary identifier.

Because the preprocessor substitutes text without knowing C's precedence or evaluation rules, macros produce well-known output pitfalls: SQUARE(2 + 3) that is not 25, or an argument evaluated twice. This note covers the directives, object-like and function-like macros and their expansion rules, the # and ## operators, conditional compilation, include guards, and the trade-offs between macros and functions.

Conventions: C11; expansions shown are those of gcc -std=c11 -E, and outputs are for gcc on x86-64 Linux.

Prerequisites

Core concepts

Where the preprocessor sits

Translation proceeds in phases. In outline: physical lines are joined where a line ends in \; the source is split into preprocessing tokens and each comment is replaced by one space; then directives are executed and macros expanded; then string literals are concatenated and the result is compiled. A directive is a line whose first non-blank character is #; it ends at the end of the line.

DirectivePurpose
#include <h> / #include "h"insert the contents of a header (the quoted form searches the including file's directory first, then the same places as <>; the exact search is implementation-defined)
#define, #undefdefine or remove a macro
#if, #elif, #else, #endifconditional compilation on an integer constant expression
#ifdef N, #ifndef Ntest whether a macro is defined (same as #if defined(N), #if !defined(N))
#error, #pragma, #linediagnostic, implementation-specific request, line renumbering

Object-like macros

C
#define PI 3.14159
#define AREA(r) (PI * (r) * (r))     /* macros may use other macros */

Every later occurrence of the identifier PI as a token is replaced by 3.14159. Replacement does not happen inside string literals or character constants, or inside a longer identifier such as PIE. There is no type and no storage; the compiler sees only the number.

Function-like macros

C
#define SQ_BAD(x) x * x
#define SQ(x) ((x) * (x))

The parameter list must follow the macro name with no space: #define F (x) x defines an object-like macro F whose replacement is (x) x, so F(3) becomes (x) x(3).

At a use such as SQ(2 + 3), the argument tokens 2 + 3 are substituted for x as tokens, not as a value:

UseExpansionValue
SQ_BAD(2 + 3)2 + 3 * 2 + 311
SQ(2 + 3)((2 + 3) * (2 + 3))25
100 / SQ_BAD(5)100 / 5 * 5100
100 / SQ(5)100 / ((5) * (5))4

Rule: parenthesise every parameter in the replacement and the whole replacement.

Arguments evaluated more than once

C
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int m = MAX(next(), 0);   /* next() increments a counter and returns it */

Assume a counter initially 0 and next() increments it by one and returns the new value. next() appears twice in the expansion. It is called once in the comparison and, because the result is greater than 0, again in the chosen branch: m is 2 and the counter is 2. A function max(next(), 0) would call next() once. With an argument like i++, the double evaluation can even be undefined behaviour: SQ(i++) expands to ((i++) * (i++)), two unsequenced modifications of i (Undefined, Unspecified and Implementation-Defined Behaviour).

Multi-statement macros

C
#define SWAP(a, b) do { int t_ = (a); (a) = (b); (b) = t_; } while (0) /* restricted int-only illustration */
if (a < b) SWAP(a, b); else a = 0;

Restrictions. The macro requires side-effect-free modifiable int lvalues and must not capture a caller identifier named t_; for example SWAP(x,t_) is unsafe. The wrapper is not general macro hygiene. For real C11 code prefer static inline void swap_int(int *a, int *b) { int t = *a; *a = *b; *b = t; } with pointers to distinct live int objects, then call swap_int(&a,&b).

The do { ... } while (0) wrapper makes the macro a single statement that still needs its trailing semicolon, so it works inside if ... else. With plain braces, SWAP(a, b); would leave an extra ; statement before else, which is a syntax error.

How it works: expansion rules

Stringizing # and token pasting ##

C
#define STR(x) #x
#define CAT(a, b) a ## b
STR(a + b)      /* becomes "a + b" */
CAT(var, 12)    /* becomes var12 */
  • #param turns the argument's spelling into a string literal.
  • a ## b joins two tokens into one; the result must be a valid token.
  • Arguments of # and ## are not macro-expanded first. Other arguments are fully expanded before substitution. To stringize the value of a macro, add a level:
C
#define N 42
#define XSTR(x) STR(x)
STR(N)     /* "N"  */
XSTR(N)    /* "42": N is expanded while being passed to XSTR, then STR stringizes 42 */

No recursion

After substitution the result is rescanned for more macro names, but a macro's own name is not replaced again while it is being expanded. So #define f(x) x + f(x) gives f(1) →\to 1 + f(1), and the remaining f(1) is left for the compiler (as a call to a function f, if one exists).

Conditional compilation

C
#ifdef DEBUG
    fprintf(stderr, "x = %d\n", x);
#endif

#if __STDC_VERSION__ >= 201112L
    /* C11 or later */
#elif defined(__STDC_VERSION__)
    /* C99 or C95 */
#else
    /* C90 */
#endif
  • #if evaluates an integer constant expression. An identifier that is not a macro counts as 0 there, so #if UNDEFINED_NAME == 0 is true.
  • Code in an inactive branch is skipped and need not compile, but it must still consist of valid preprocessing tokens.
  • gcc -DDEBUG defines DEBUG from the command line, which selects the debug branch.

Include guards

A header may be included more than once in a translation unit, directly or through other headers. Defining the same structure type twice is an error, so headers are guarded:

C
/* point.h */
#ifndef POINT_H
#define POINT_H
struct pt { int x, y; };
#endif

The second inclusion finds POINT_H already defined and skips the body. Without a guard, including such a header twice makes gcc stop with the error "redefinition of 'struct pt'".

Predefined macros

__FILE__ (current file name as a string), __LINE__ (current line number), __DATE__, __TIME__, __STDC__ (1 on a conforming implementation), and __STDC_VERSION__ (201112L for C11). They are useful in diagnostics such as assert, which is itself a macro.

Macros versus functions

AspectFunction-like macroFunction
Type checking of argumentsnoneby the prototype
Argument evaluationexpansion can duplicate argument tokens; runtime count depends on the resulting expression and executed patheach evaluated function argument is evaluated once before the call
Precedence safetyonly with full parenthesisationautomatic
Call overheadnone (code is inlined textually)a call, unless inlined by the compiler
Code sizegrows with each useone copy
Generic over typesyes (MAX works for int and double)no (C11 _Generic can help)
Address, recursion, debuggingnoneyes

C99 inline functions give most of the speed of macros with the safety of functions.

Worked examples

All expansions below are those of gcc -E; all outputs are for C11 with gcc on x86-64 Linux.

Example 1. SQ_BAD(2 + 3) prints 11, SQ(2 + 3) prints 25, 100 / SQ(5) prints 4.

Example 2. 10 * DBL(3) with #define DBL(x) (x) + (x) expands to 10 * (3) + (3), which is 33, not 60, because the whole replacement was not parenthesised.

Example 3. MAX(next(), 0) gives 2 with the counter at 2.

Example 4. if (a < b) SWAP(a, b); else a = 0; with a = 1, b = 2 leaves a = 2, b = 1.

Example 5. STR(N) is "N"; XSTR(N) is "42".

Example 6. AREA(1.0) prints 3.14 with %.2f; __STDC_VERSION__ is 201112L under -std=c11.

Important properties and observations

  • The preprocessor works on tokens; it knows nothing of types, scope or precedence.
  • Parenthesise each macro parameter and the whole replacement.
  • Never pass arguments with side effects to a function-like macro.
  • In its #define directive, no whitespace may separate a function-like macro's name from (. Whitespace before ( is allowed when invoking that macro.
  • A macro does not expand recursively.
  • Undefined identifiers in #if evaluate to 0.
  • Guard every header with #ifndef / #define / #endif.

GATE examination perspective

  • Output of a program using a macro without parentheses: expand by hand, then apply C precedence (Operators and Expressions).
  • Double evaluation of arguments with side effects.
  • # and ## results, and nested macro expansion.
  • Which branch of #if / #ifdef is compiled.
  • Macro versus function trade-offs.

Summary

Sources

  1. ISO/IEC 9899:2011 (C11), committee draft N1570, (consulted 2026-11-09), Section 5.1.1.2 (translation phases), 6.10 (preprocessing directives), 6.10.1 (conditional inclusion), 6.10.2 (source file inclusion), 6.10.3 (macro replacement), 6.10.3.2 (the # operator), 6.10.3.3 (the ## operator), 6.10.3.4 (rescanning and further replacement), 6.10.8 (predefined macro names).
  2. B. W. Kernighan and D. M. Ritchie, The C Programming Language, 2nd ed., Prentice Hall, 1988: Section 4.11 (the C preprocessor).

Next steps