C Programming

Chapter 13 – Preprocessors and Macros in C Language

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

DirectivePurpose
#includeIncludes a header file
#defineDefines a macro
#undefRemoves a macro definition
#ifConditional compilation
#ifdefTests whether a macro is defined
#ifndefTests whether a macro is not defined
#elseAlternative conditional section
#elifAdditional conditional condition
#endifEnds conditional compilation
#errorGenerates a preprocessing diagnostic
#pragmaProvides 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

FeatureMacroFunction
Processed byPreprocessorCompiler
Type checkingNot performed like a function callCompiler checks parameter types
Parameter evaluationCan occur multiple timesArguments are evaluated once per parameter expression
Return typeNo declared return typeHas a return type
DebuggingCan be more difficultUsually easier
Typical useConditional compilation, symbolic replacementReusable 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

PreprocessorCompiler
Handles preprocessing directivesCompiles C language constructs
Processes #includeChecks C syntax and semantics
Expands macrosGenerates lower-level code
Handles conditional compilationPerforms compilation and optimization
Works before normal compilationProcesses 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

ConceptMeaning
PreprocessorProcesses preprocessing directives before compilation
DirectiveInstruction beginning with #
#includeIncludes a header
#defineDefines a macro
#undefRemoves a macro definition
#ifConditional preprocessing
#ifdefTests whether a macro is defined
#ifndefTests whether a macro is not defined
#elseAlternative branch
#elifAdditional condition
#endifEnds conditional block
#errorProduces a preprocessing diagnostic
#pragmaImplementation-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.