Chapter 13 – Preprocessors and Macros in C Language
Introduction
The C preprocessor is an important part of the C programming environment. It processes certain instructions in a source file before the compiler performs the normal compilation of the C code.
Preprocessor instructions are called preprocessor directives. They generally begin with the # symbol.
Some commonly used directives are:
#include #define #undef #if #ifdef #ifndef #else #elif #endif
Preprocessors are commonly used for:
Including header files
Creating constants
Defining macros
Conditional compilation
Preventing repeated inclusion of headers
Writing configurable and maintainable programs
1. What is a Preprocessor?
The C preprocessor is a program or compilation phase that handles preprocessing directives before the source code is compiled.
For example:
#include <stdio.h>
tells the preprocessing stage to make the declarations from the standard I/O header available to the source file.
A simplified compilation process can be viewed as:
C Source Code ↓ Preprocessing ↓ Compilation ↓ Assembly ↓ Linking ↓ Executable Program
The exact implementation details can vary, but preprocessing occurs before the compiler processes the resulting translation unit.
2. What is a Preprocessor Directive?
A preprocessor directive is an instruction beginning with #.
Example:
#define PI 3.14159
The directive tells the preprocessor to define a macro named PI.
Unlike ordinary C statements, preprocessor directives do not normally end with a semicolon.
Correct:
#define MAX 100
Incorrect:
#define MAX 100;
3. Common Preprocessor Directives
| Directive | Purpose |
|---|---|
| #include | Includes a header file |
| #define | Defines a macro |
| #undef | Removes a macro definition |
| #if | Conditional compilation |
| #ifdef | Tests whether a macro is defined |
| #ifndef | Tests whether a macro is not defined |
| #else | Alternative conditional section |
| #elif | Additional conditional condition |
| #endif | Ends conditional compilation |
| #error | Generates a preprocessing diagnostic |
| #pragma | Provides implementation-specific instructions |
4. #include Directive
The #include directive is used to include the contents or declarations of another file during preprocessing.
Example:
#include <stdio.h>
This provides access to declarations such as printf().
Another example:
#include <stdlib.h>
This provides declarations for functions such as:
malloc() calloc() realloc() free()
5. Angle Brackets and Double Quotes
Two common forms are:
#include <stdio.h>
and:
#include "myheader.h"
< >
Usually used for system or implementation-provided headers.
Example:
#include <stdio.h>
" "
Commonly used for headers created by the programmer.
Example:
#include "student.h"
The exact search rules are defined by the C implementation, but this distinction is a useful general convention.
6. Creating Your Own Header File
Suppose we create a file called:
mathutils.h
It might contain:
int square(int n) { return n * n; }
Another source file can include it:
#include <stdio.h> #include "mathutils.h" int main() { printf("%d", square(5)); return 0; }
Output
25
In real projects, function declarations are commonly placed in header files while function definitions are placed in .c source files.
7. What is a Macro?
A macro is a name defined using #define.
Example:
#define PI 3.14159
Now:
printf("%f", PI);
can be preprocessed using the macro replacement.
Macros are handled by the preprocessor rather than functioning like ordinary variables.
8. Object-Like Macros
A macro without parameters is called an object-like macro.
Example:
#define MAX_STUDENTS 50 #define COUNTRY "India" #define PASS_MARKS 33
Program:
#include <stdio.h> #define PASS_MARKS 33 int main() { int marks = 75; if(marks >= PASS_MARKS) { printf("Pass"); } return 0; }
Output
Pass
9. Why Use Macros?
Macros can make code easier to maintain when a symbolic value is needed in multiple places.
Instead of writing:
if(marks >= 33)
in many places, we can write:
if(marks >= PASS_MARKS)
and define:
#define PASS_MARKS 33
If the value needs to change, there is a single definition to update.
For typed constants in modern C, however, const variables or enumerations are often preferable to macros when a macro is not specifically needed.
10. Function-Like Macros
A macro can also accept parameters.
Example:
#define SQUARE(x) ((x) * (x))
Program:
#include <stdio.h> #define SQUARE(x) ((x) * (x)) int main() { printf("%d", SQUARE(5)); return 0; }
Output
25
11. Why Use Parentheses in Macros?
Consider:
#define SQUARE(x) x * x
If we write:
SQUARE(2 + 3)
the replacement can behave like:
2 + 3 * 2 + 3
which does not produce the intended mathematical result.
A safer macro is:
#define SQUARE(x) ((x) * (x))
Now:
SQUARE(2 + 3)
expands appropriately to an expression equivalent to:
((2 + 3) * (2 + 3))
12. Another Macro Example
#define MAX(a, b) ((a) > (b) ? (a) : (b))
Program:
#include <stdio.h> #define MAX(a, b) ((a) > (b) ? (a) : (b)) int main() { printf("%d", MAX(10, 20)); return 0; }
Output
20
13. Important Limitation of Function-Like Macros
Macros do not behave exactly like functions.
Consider:
#define SQUARE(x) ((x) * (x))
If we use:
SQUARE(i++)
the argument may be evaluated more than once.
That can lead to unexpected behavior.
Therefore, function-like macros must be designed carefully.
For many calculations, an ordinary function is safer.
Example:
int square(int x) { return x * x; }
14. Macro vs Function
| Feature | Macro | Function |
|---|---|---|
| Processed by | Preprocessor | Compiler |
| Type checking | Not performed like a function call | Compiler checks parameter types |
| Parameter evaluation | Can occur multiple times | Arguments are evaluated once per parameter expression |
| Return type | No declared return type | Has a return type |
| Debugging | Can be more difficult | Usually easier |
| Typical use | Conditional compilation, symbolic replacement | Reusable operations |
15. #undef Directive
The #undef directive removes a macro definition.
Example:
#define SIZE 100 #undef SIZE
After #undef SIZE, the macro SIZE is no longer defined in the subsequent preprocessing region.
Example:
#include <stdio.h> #define VALUE 100 #undef VALUE int main() { printf("Macro removed."); return 0; }
16. Conditional Compilation
Conditional compilation allows parts of a program to be included or excluded depending on preprocessing conditions.
Important directives include:
#if #ifdef #ifndef #else #elif #endif
17. #if Directive
The #if directive tests a preprocessing constant expression.
Example:
#include <stdio.h> #define VERSION 2 #if VERSION == 2 #define MESSAGE "Version 2" #endif int main() { printf("%s", MESSAGE); return 0; }
Output
Version 2
18. #else Directive
Example:
#include <stdio.h> #define VERSION 1 #if VERSION == 2 #define MESSAGE "Version 2" #else #define MESSAGE "Other Version" #endif int main() { printf("%s", MESSAGE); return 0; }
Output
Other Version
19. #elif Directive
#elif means else if in conditional preprocessing.
Example:
#include <stdio.h> #define VERSION 3 #if VERSION == 1 #define MESSAGE "Version 1" #elif VERSION == 2 #define MESSAGE "Version 2" #elif VERSION == 3 #define MESSAGE "Version 3" #else #define MESSAGE "Unknown Version" #endif int main() { printf("%s", MESSAGE); return 0; }
Output
Version 3
20. #ifdef
#ifdef checks whether a macro is defined.
Example:
#include <stdio.h> #define DEBUG #ifdef DEBUG #define MESSAGE "Debug mode enabled" #else #define MESSAGE "Debug mode disabled" #endif int main() { printf("%s", MESSAGE); return 0; }
Since DEBUG is defined, the first branch is selected.
21. #ifndef
#ifndef checks whether a macro is not defined.
Example:
#ifndef MAX_SIZE #define MAX_SIZE 100 #endif
This means:
Define MAX_SIZE only if it has not already been defined.
22. Header Guards
Header guards are commonly used to prevent the contents of a header from being processed multiple times in a way that causes duplicate definitions.
Example:
#ifndef STUDENT_H #define STUDENT_H struct Student { int rollNo; char name[50]; }; #endif
If student.h is included more than once in a translation unit, the guard prevents the declarations inside from being processed again after the macro has been defined.
23. Why Are Header Guards Important?
Suppose several source files include the same header directly or indirectly.
Without protection, repeated declarations or definitions may create compilation problems.
A common pattern is:
#ifndef MYHEADER_H #define MYHEADER_H /* Header contents */ #endif
Header guards are a standard and portable technique.
24. #error Directive
The #error directive can be used to generate a preprocessing diagnostic.
Example:
#ifndef VERSION #error "VERSION must be defined" #endif
If VERSION has not been defined, preprocessing will report an error.
This can be useful for checking required configuration.
25. #pragma
The #pragma directive provides implementation-specific instructions to the compiler or preprocessing system.
Example:
#pragma once
Some compilers support #pragma once as an alternative way of ensuring a header is included only once.
However, #pragma behavior is generally implementation-specific, unlike standard directives such as #include and #define.
For maximum portability, traditional header guards are widely used.
26. Predefined Macros
C implementations provide several predefined macros.
Common examples include:
__FILE__ __LINE__ __DATE__ __TIME__
These can provide information about the source file and compilation environment.
27. __FILE__
__FILE__ expands to a string representing the current source file name.
Example:
#include <stdio.h> int main() { printf("File: %s\n", __FILE__); return 0; }
The exact displayed path or file name depends on the compiler and build environment.
28. __LINE__
__LINE__ expands to the current source line number.
Example:
#include <stdio.h> int main() { printf("Line: %d\n", __LINE__); return 0; }
The number depends on where the statement appears in the source file.
29. __DATE__ and __TIME__
These predefined macros can represent the date and time at which the source file is compiled.
Example:
#include <stdio.h> int main() { printf("Compiled on: %s\n", __DATE__); printf("Compiled at: %s\n", __TIME__); return 0; }
The actual values depend on the compilation environment.
30. Stringizing Operator #
Within a function-like macro, # can convert a macro argument into a string literal.
Example:
#include <stdio.h> #define SHOW(x) printf("%s = %d\n", #x, (x)) int main() { int marks = 85; SHOW(marks); return 0; }
Output
marks = 85
Here:
#x
turns the argument into text.
31. Token-Pasting Operator ##
The ## operator joins two preprocessing tokens.
Example:
#define JOIN(a, b) a##b
Then:
int JOIN(num, 1) = 50;
can produce a token equivalent to:
int num1 = 50;
This feature is useful in some generic macro techniques.
32. Multi-Line Macros
A macro can extend across multiple source lines using a backslash \.
Example:
#define PRINT_INFO() \ printf("Name: Rahul\n"); \ printf("Marks: 85\n");
Usage:
PRINT_INFO();
However, multi-line macros should be written carefully because they can introduce control-flow and debugging complexities.
33. Macro with a do...while Pattern
When a macro contains multiple statements, a common safe pattern is:
#define SHOW_MESSAGE() do { \ printf("Hello\n"); \ printf("Welcome\n"); \ } while (0)
Then:
SHOW_MESSAGE();
behaves syntactically more like a single statement.
This pattern is especially useful when the macro is used inside if statements.
34. Conditional Compilation for Debugging
Preprocessor directives can be useful for enabling debugging code.
Example:
#include <stdio.h> #define DEBUG #ifdef DEBUG #define LOG(message) printf("DEBUG: %s\n", message) #else #define LOG(message) #endif int main() { LOG("Program started"); printf("Program running."); return 0; }
If DEBUG is defined, the logging statement is included.
If it is not defined, the LOG() macro expands to nothing.
35. Compile-Time Configuration
Conditional compilation can be used to create different versions of a program.
For example:
#define VERSION 2 #if VERSION == 1 /* Code for version 1 */ #elif VERSION == 2 /* Code for version 2 */ #endif
This can be useful when the same source code must support different configurations.
36. defined Operator
The defined operator can be used with conditional preprocessing.
Example:
#if defined(DEBUG) printf("Debug mode"); #endif
It can also be written as:
#ifdef DEBUG printf("Debug mode"); #endif
Both forms are useful, although #ifdef is simpler for a single macro test.
37. Nested Conditional Compilation
Conditional directives can be nested.
Example:
#define VERSION 2 #define DEBUG #if VERSION == 2 #ifdef DEBUG #define MESSAGE "Version 2 Debug" #else #define MESSAGE "Version 2" #endif #endif
Nested conditions should be formatted carefully so that the structure remains easy to understand.
38. Common Macro Naming Convention
Macros are often written in uppercase letters:
#define MAX_SIZE 100 #define PI 3.14159 #define PASS_MARKS 33
This makes them visually distinct from variables and functions.
It is a convention rather than a language requirement.
39. Macro Constant vs const
Consider:
#define MAX_STUDENTS 50
and:
const int maxStudents = 50;
They are not equivalent.
A macro performs preprocessing replacement.
A const object is an actual C object with a type and storage characteristics determined by the language and implementation.
For many typed constants, const is preferable because the compiler can apply normal type rules.
Use macros when preprocessing behavior is actually needed.
40. Common Mistakes with Macros
Mistake 1: Forgetting parentheses
Avoid:
#define SQUARE(x) x*x
Prefer:
#define SQUARE(x) ((x)*(x))
Mistake 2: Adding a semicolon to a simple macro
Avoid:
#define PI 3.14;
Prefer:
#define PI 3.14
Mistake 3: Unexpected multiple evaluation
Avoid using expressions with side effects as arguments to macros that evaluate their parameters multiple times.
For example:
SQUARE(i++)
can produce unexpected behavior.
Mistake 4: Overusing macros
Not everything needs to be a macro.
Ordinary functions, const objects, and enumerations are often clearer and safer alternatives.
41. Complete Example
The following program demonstrates several preprocessing concepts:
#include <stdio.h> #define PASS_MARKS 33 #define SQUARE(x) ((x) * (x)) int main() { int marks = 75; #if PASS_MARKS > 0 if(marks >= PASS_MARKS) { printf("Student has passed.\n"); } #endif printf("Square of 6 = %d\n", SQUARE(6)); #ifdef DEBUG printf("Debug mode is enabled.\n"); #endif return 0; }
Output
Student has passed. Square of 6 = 36
The debug message is not displayed because DEBUG was not defined.
42. Preprocessor vs Compiler
| Preprocessor | Compiler |
|---|---|
| Handles preprocessing directives | Compiles C language constructs |
| Processes #include | Checks C syntax and semantics |
| Expands macros | Generates lower-level code |
| Handles conditional compilation | Performs compilation and optimization |
| Works before normal compilation | Processes the resulting translation unit |
The overall toolchain contains additional stages, such as assembling and linking.
43. Important Preprocessor Directives
Include
#include <stdio.h>
Define
#define PI 3.14
Undefine
#undef PI
Conditional
#if CONDITION #endif
Defined check
#ifdef DEBUG #endif
Not defined
#ifndef HEADER_H #endif
Alternative
#else
Else-if
#elif CONDITION
44. Quick Revision Table
| Concept | Meaning |
|---|---|
| Preprocessor | Processes preprocessing directives before compilation |
| Directive | Instruction beginning with # |
| #include | Includes a header |
| #define | Defines a macro |
| #undef | Removes a macro definition |
| #if | Conditional preprocessing |
| #ifdef | Tests whether a macro is defined |
| #ifndef | Tests whether a macro is not defined |
| #else | Alternative branch |
| #elif | Additional condition |
| #endif | Ends conditional block |
| #error | Produces a preprocessing diagnostic |
| #pragma | Implementation-specific instruction |
| # | Stringizes a macro argument |
| ## | Concatenates preprocessing tokens |
45. Practice MCQs
Question 1
Which symbol is used to begin a preprocessor directive?
A. $
B. @
C. #
D. &
Answer: C. #
Question 2
Which directive is used to include a header file?
A. #import
B. #include
C. #header
D. #using
Answer: B. #include
Question 3
Which directive is used to define a macro?
A. #macro
B. #define
C. #constant
D. #create
Answer: B. #define
Question 4
Which directive removes a macro definition?
A. #remove
B. #delete
C. #undef
D. #clear
Answer: C. #undef
Question 5
Which directive checks whether a macro is defined?
A. #ifdef
B. #ifnot
C. #check
D. #defined
Answer: A. #ifdef
Question 6
Which directive checks whether a macro is not defined?
A. #ifnot
B. #ifndef
C. #notdefined
D. #else
Answer: B. #ifndef
Question 7
Which operator is used for token concatenation in macros?
A. #
B. ##
C. &&
D. ++
Answer: B. ##
Question 8
Which operator converts a macro argument into a string literal?
A. ##
B. #
C. %
D. $
Answer: B. #
Question 9
Which predefined macro identifies the current source file?
A. __NAME__
B. __FILE__
C. __SOURCE__
D. __PROGRAM__
Answer: B. __FILE__
Question 10
Which predefined macro represents the current source line number?
A. __ROW__
B. __LINE__
C. __NUMBER__
D. __COUNT__
Answer: B. __LINE__
46. Programming Exercises
Practice the following programs:
Define a macro for the value of PI.
Create a macro to calculate the square of a number.
Create a macro to find the larger of two numbers.
Use #ifdef to create a debug mode.
Use #ifndef to create a header guard.
Demonstrate the use of #undef.
Display __FILE__ and __LINE__.
Create a custom header file and include it in a C program.
Use conditional compilation to create two versions of a program.
Create a macro that converts an expression into a string.
Create a macro using the ## token-pasting operator.
Create a small C program demonstrating multiple preprocessing directives.
47. Key Points to Remember
The preprocessor handles preprocessing directives before normal compilation.
Preprocessor directives generally begin with #.
#include is used to include headers.
#define creates macros.
#undef removes a macro definition.
#if, #ifdef, and #ifndef are used for conditional compilation.
#else, #elif, and #endif control conditional sections.
# can stringify a macro argument.
## can concatenate preprocessing tokens.
Header guards help prevent repeated inclusion of a header.
__FILE__, __LINE__, __DATE__, and __TIME__ are commonly available predefined macros.
Function-like macros should use parentheses carefully.
Macros with side-effect expressions can produce unexpected results.
Use macros when preprocessing is useful; otherwise, ordinary functions, const, or enumerations may be better choices.
Chapter Summary
The C preprocessor provides features that operate before the compiler processes the C program. It is particularly useful for including header files, defining macros, and controlling which portions of source code are compiled.
The most important directives are:
#include #define #undef #if #ifdef #ifndef #else #elif #endif
Macros can represent constants or expressions, while conditional compilation can be used for debugging and platform/configuration-specific code.
Understanding preprocessing is important because it prepares the foundation for large C projects, reusable header files, conditional compilation, debugging configurations, and advanced macro techniques.
Next Chapter
Chapter 14 – Command Line Arguments in C Language
In the next chapter, you will learn how a C program can receive information directly from the command line using argc and argv, along with practical examples and programs.