Chapter 15: The Preprocessor

📌 Learning Objectives
  • LO 15.1: Understand the role of the C preprocessor
  • LO 15.2: Implement macro substitution using #define
  • LO 15.3: Use file inclusion directives (#include)
  • LO 15.4: Apply conditional compilation directives
  • LO 15.5: Work with ANSI additions like #pragma and #error

15.1 Introduction to the Preprocessor

The C preprocessor is a program that processes the source code before it passes through the compiler. It operates under the control of preprocessor directives, which begin with the # symbol in column one.

Key characteristics of preprocessor directives:

15.2 Preprocessor Directives

Directive Function
#define Defines a macro substitution
#undef Undefines a macro
#include Specifies files to be included
#ifdef Tests if a macro is defined
#ifndef Tests if a macro is not defined
#if Tests a compile-time condition
#else Provides alternatives when #if fails
#elif Establishes if-else-if sequence
#endif Specifies end of #if
#error Produces diagnostic messages
#pragma Implementation-oriented instructions

15.3 Macro Substitution (#define)

Macro substitution is a process where an identifier is replaced by a predefined string. The general form is:

#define identifier string

Simple Macro Substitution

#define PI 3.14159
#define STRENGTH 100
#define PASS_MARK 50

area = PI * r * r;
if (marks >= PASS_MARK) ...

📝 Worked-Out Problem 15.1

Using symbolic constants with #define:

#include <stdio.h>
#define PI 3.14159
#define HEIGHT 10

int main() {
    float radius, area, volume;
    
    printf("Enter radius: ");
    scanf("%f", &radius);
    
    area = PI * radius * radius;
    volume = area * HEIGHT;
    
    printf("Area of circle = %.2f\n", area);
    printf("Volume of cylinder = %.2f\n", volume);
    
    return 0;
}
Output:
Enter radius: 5
Area of circle = 78.54
Volume of cylinder = 785.40
⚠️ Important:
  • No semicolon at the end of #define line
  • The # symbol must be in the first column
  • Macros are not variables - cannot be changed by assignment
  • Use parentheses to avoid evaluation errors

Macros with Arguments

Macros can take arguments, making them behave like functions (but with text substitution).

#define CUBE(x) ((x) * (x) * (x))
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define IS_EVEN(x) ((x) % 2 == 0)

📝 Worked-Out Problem 15.2

Macros with arguments - importance of parentheses:

#include <stdio.h>

// Without parentheses - WRONG!
#define SQUARE_BAD(x) x * x

// With parentheses - CORRECT
#define SQUARE(x) ((x) * (x))

int main() {
    int a = 5, b = 2;
    
    printf("SQUARE(5) = %d\n", SQUARE(5));
    printf("SQUARE(a+b) = %d\n", SQUARE(a+b));
    printf("SQUARE_BAD(a+b) = %d\n", SQUARE_BAD(a+b));  // Wrong!
    
    printf("MAX(10,20) = %d\n", MAX(10,20));
    
    return 0;
}
Output:
SQUARE(5) = 25
SQUARE(a+b) = 49
SQUARE_BAD(a+b) = 17
MAX(10,20) = 20

Note: SQUARE_BAD(5+2) expands to 5+2*5+2 = 5+10+2 = 17, not 49!

Nesting of Macros

Macros can be nested, with one macro used in the definition of another.

#define SQUARE(x) ((x) * (x))
#define CUBE(x) (SQUARE(x) * (x))
#define FOURTH(x) (SQUARE(x) * SQUARE(x))

Undefining a Macro: #undef

A macro can be undefined using #undef, which restricts its definition to a particular part of the program.

#define TEMP 100
/* TEMP can be used here */
#undef TEMP
/* TEMP is no longer defined */

15.4 File Inclusion (#include)

The #include directive inserts the contents of another file into the source code.

Two forms:

#include <filename>    // Searches in standard directories
#include "filename"    // Searches in current directory first, then standard

Common header files:

Header FilePurpose
<stdio.h>Standard I/O functions
<stdlib.h>Memory allocation, conversions
<string.h>String handling functions
<math.h>Mathematical functions
<ctype.h>Character handling functions
<time.h>Date and time functions

📝 Worked-Out Problem 15.3

Creating and using user-defined header files:

/* myheader.h */
#define PI 3.14159
#define MAX(a,b) ((a) > (b) ? (a) : (b))
#define MIN(a,b) ((a) < (b) ? (a) : (b))

/* main.c */
#include <stdio.h>
#include "myheader.h"

int main() {
    int x = 10, y = 20;
    float r = 5.5;
    
    printf("PI = %.5f\n", PI);
    printf("Area of circle = %.2f\n", PI * r * r);
    printf("MAX(%d,%d) = %d\n", x, y, MAX(x,y));
    printf("MIN(%d,%d) = %d\n", x, y, MIN(x,y));
    
    return 0;
}
Output:
PI = 3.14159
Area of circle = 95.03
MAX(10,20) = 20
MIN(10,20) = 10

15.5 Conditional Compilation

Conditional compilation directives allow parts of the code to be compiled only if certain conditions are met.

#ifdef and #ifndef

#ifdef TEST
    printf("Debugging mode\n");
#endif

#ifndef HEADER_H
#define HEADER_H
/* header file contents */
#endif

📝 Worked-Out Problem 15.4

Using #ifdef for debugging:

#include <stdio.h>

#define DEBUG  // Comment this line to disable debugging

int main() {
    int x = 10, y = 20, z;
    
    z = x * y;
    
    #ifdef DEBUG
        printf("Debug: x = %d, y = %d\n", x, y);
        printf("Debug: z = %d\n", z);
    #endif
    
    printf("Result = %d\n", z);
    
    return 0;
}
With DEBUG defined:
Debug: x = 10, y = 20
Debug: z = 200
Result = 200

With DEBUG commented:
Result = 200

#if, #else, #elif Directives

#if MACHINE == 1
    #include "machine1.h"
#elif MACHINE == 2
    #include "machine2.h"
#else
    #include "default.h"
#endif

📝 Worked-Out Problem 15.5

Conditional compilation for different systems:

#include <stdio.h>

#define SYSTEM 2  // 1 for Windows, 2 for Linux, 3 for Mac

int main() {
    #if SYSTEM == 1
        printf("Running on Windows\n");
        printf("Using Windows-specific features\n");
    #elif SYSTEM == 2
        printf("Running on Linux\n");
        printf("Using Linux-specific features\n");
    #elif SYSTEM == 3
        printf("Running on Mac\n");
        printf("Using Mac-specific features\n");
    #else
        printf("Unknown system\n");
    #endif
    
    return 0;
}
Output:
Running on Linux
Using Linux-specific features

Using #if with expressions

#if VERSION > 3
    /* New features */
#endif

#if defined(DEBUG) && LEVEL > 1
    /* Detailed debugging */
#endif

15.6 ANSI Additions

The #elif Directive

Combines else and if for multiple conditions:

#if OS == 1
    /* code for OS 1 */
#elif OS == 2
    /* code for OS 2 */
#elif OS == 3
    /* code for OS 3 */
#else
    /* default code */
#endif

The #pragma Directive

Provides implementation-oriented instructions to the compiler. Syntax varies by compiler.

#pragma once               // Include file only once
#pragma pack(1)            // Pack structures tightly
#pragma warning(disable:4996)  // Disable specific warnings
💡 Note: #pragma directives are compiler-specific and may not be portable across different compilers.

The #error Directive

Produces a diagnostic message and terminates compilation.

#ifndef HEADER_FILE
#error "HEADER_FILE is not defined"
#endif

Stringizing Operator (#)

The # operator converts a macro parameter to a string constant.

#define STR(x) #x
#define PRINT(x) printf(#x " = %d\n", x)

int main() {
    int value = 10;
    printf("%s\n", STR(Hello));  // Prints "Hello"
    PRINT(value);                 // Prints "value = 10"
    return 0;
}

📝 Worked-Out Problem 15.6

Stringizing operator demonstration:

#include <stdio.h>

#define TO_STRING(x) #x
#define PRINT_INT(x) printf(#x " = %d\n", x)
#define PRINT_FLOAT(x) printf(#x " = %.2f\n", x)

int main() {
    int age = 25;
    float salary = 45000.50;
    
    printf("%s\n", TO_STRING(Hello World));
    PRINT_INT(age);
    PRINT_FLOAT(salary);
    
    return 0;
}
Output:
Hello World
age = 25
salary = 45000.50

Token Pasting Operator (##)

The ## operator concatenates two tokens to form a single token.

#define CONCAT(a,b) a##b
#define MAKE_VAR(name, num) name##num

int main() {
    int CONCAT(var,1) = 10;  // creates var1 = 10
    int MAKE_VAR(x,5) = 20;   // creates x5 = 20
    printf("var1 = %d, x5 = %d\n", var1, x5);
    return 0;
}

📝 Worked-Out Problem 15.7

Token pasting for creating variables:

#include <stdio.h>

#define CREATE_VAR(name, num) name##num
#define PRINT_VAR(name, num) printf(#name #num " = %d\n", name##num)

int main() {
    int CREATE_VAR(score,1) = 95;
    int CREATE_VAR(score,2) = 87;
    int CREATE_VAR(score,3) = 92;
    
    PRINT_VAR(score,1);
    PRINT_VAR(score,2);
    PRINT_VAR(score,3);
    
    return 0;
}
Output:
score1 = 95
score2 = 87
score3 = 92

15.7 Predefined Macros

ANSI C defines several predefined macros that provide useful information:

MacroValue
__LINE__Current line number
__FILE__Current file name
__DATE__Compilation date (MMM DD YYYY)
__TIME__Compilation time (HH:MM:SS)
__STDC__1 if ANSI C standard is followed

📝 Worked-Out Problem 15.8

Using predefined macros:

#include <stdio.h>

int main() {
    printf("File: %s\n", __FILE__);
    printf("Line: %d\n", __LINE__);
    printf("Date: %s\n", __DATE__);
    printf("Time: %s\n", __TIME__);
    printf("ANSI C: %d\n", __STDC__);
    
    #line 100 "newfile.c"
    printf("Now at line %d in file %s\n", __LINE__, __FILE__);
    
    return 0;
}
Output:
File: program.c
Line: 6
Date: Mar 15 2024
Time: 14:30:25
ANSI C: 1
Now at line 100 in file newfile.c

Important Points to Remember

  • Preprocessor directives begin with # in column one.
  • No semicolon is used at the end of preprocessor directives.
  • Use parentheses liberally in macros with arguments to avoid evaluation errors.
  • Macros are text substitutions, not functions - be careful with side effects.
  • Use #include <file> for standard headers and #include "file" for user-defined headers.
  • Conditional compilation is useful for debugging, portability, and multiple configurations.
  • Stringizing (#) and token pasting (##) are powerful but can make code harder to read.
  • Predefined macros help with debugging and program information.

Chapter Exercises

Review Questions

  1. State true or false:
    a) The keyword #define must be written starting from the first column.
    b) Like other statements, preprocessor directives must end with a semicolon.
    c) All preprocessor directives begin with #.
    d) We cannot use a macro in the definition of another macro.
    e) Preprocessor directives are executed by the compiler with the same priority as other statements.
    f) #include <stdio.h> checks for stdio.h in the standard directory only.
  2. What is a macro? How is it different from a variable?
  3. What precautions should be taken when using macros with arguments?
  4. What is conditional compilation? How does it help?
  5. Distinguish between #ifdef and #if directives.
  6. Why are some file names enclosed in angle brackets while others in double quotes?

Multiple Choice Questions

  1. Which is a file inclusion preprocessor directive?
    a) #include
    b) #define
    c) #pragma
    d) Both 1 and 2
  2. Which statement about compiler control directives is incorrect?
    a) #ifdef must be followed by #endif
    b) #ifndef must be followed by #endif
    c) #undef undefines a macro
    d) #if must be followed by #else
  3. Which directive provides alternative test facility by establishing if-else-if sequence?
    a) #elif
    b) #if
    c) #else
    d) #pragma
  4. The operator ________ converts its operand to a string.
    a) #
    b) ##
    c) %
    d) $
  5. The ________ directive causes implementation-oriented action.
    a) #error
    b) #pragma
    c) #line
    d) #include

Debugging Exercises

Find errors in the following macro definitions:

#define until(x) while(!x)  // Valid?

#define ABS(x) (x > 0) ? (x) : (-x)  // Missing parentheses?

#ifdef(FLAG)  // Error?
    #undef FLAG
#endif

#if n == 1 update(item)  // Missing parentheses?
    #else print(item)
#endif

#define SQUARE(x) x * x  // What's wrong with this?

Programming Exercises

  1. Define a macro PRINT_VALUE that can print two values of arbitrary type.
  2. Write a nested macro that gives the minimum of three values.
  3. Define a macro with one parameter to compute the volume of a sphere. Write a program to compute volumes for radii 5, 10, and 15.
  4. Define a macro that receives an array and the number of elements and prints all elements.
  5. Write symbolic constants for binary arithmetic operators +, -, *, /. Write a program to illustrate their use.
  6. Define symbolic constants for { and } and use them in a program.
  7. Write a program to illustrate the stringizing operator.
  8. Write a C program that uses the #error directive to stop execution when an error is encountered.
  9. Write a program using macro substitution to compute the area of a rectangle.
  10. Implement a macro named NOTEQUAL that performs logical NOT operation.

Interview Questions

  1. What will be the types of i1 and i2 in:
    #define INTPTR int*
    INTPTR i1, i2;
  2. What is the difference between #include <file> and #include "file"?
  3. What will be the output of:
    #define X -Y
    #define Y -Z
    #define Z 10
    printf("%d", X);
  4. What is the difference between a macro and a function?
  5. What are the advantages and disadvantages of using macros?
  6. What does #pragma once do?
  7. What is the purpose of the #line directive?

Case Study: Conditional Compilation for Different Platforms

📝 Case Study: Cross-platform development using conditional compilation

#include <stdio.h>

// Define the target platform - uncomment one
#define WINDOWS
// #define LINUX
// #define MAC

// Platform-specific headers
#ifdef WINDOWS
    #include <windows.h>
    #define CLEAR "cls"
#elif defined(LINUX) || defined(MAC)
    #include <unistd.h>
    #define CLEAR "clear"
#endif

// Platform-specific functions
#ifdef WINDOWS
    #define SLEEP(ms) Sleep(ms)
#else
    #define SLEEP(ms) usleep((ms) * 1000)
#endif

// Debug levels
#define DEBUG_LEVEL 2

void log_message(int level, const char *msg) {
    #if DEBUG_LEVEL >= level
        printf("[LOG] %s\n", msg);
    #endif
}

int main() {
    printf("Running on ");
    
    #ifdef WINDOWS
        printf("Windows\n");
    #elif defined(LINUX)
        printf("Linux\n");
    #elif defined(MAC)
        printf("Mac\n");
    #else
        printf("Unknown platform\n");
    #endif
    
    log_message(1, "Program started");
    printf("Waiting...\n");
    SLEEP(2000);
    log_message(2, "Sleep completed");
    
    // Use the appropriate clear command
    // system(CLEAR);  // Would clear the screen
    
    printf("Done!\n");
    log_message(1, "Program ended");
    
    return 0;
}

Case Study 2: Debugging Macros

📝 Case Study: Creating comprehensive debugging macros

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

// Debug levels
#define DEBUG_NONE  0
#define DEBUG_ERROR 1
#define DEBUG_WARN  2
#define DEBUG_INFO  3
#define DEBUG_ALL   4

// Set debug level
#define DEBUG_LEVEL DEBUG_ALL

// Timestamp macro
#define TIMESTAMP() \
    do { \
        time_t now = time(NULL); \
        printf("[%s]", ctime(&now)); \
    } while(0)

// Debug printing macros
#if DEBUG_LEVEL >= DEBUG_ERROR
    #define LOG_ERROR(msg) \
        do { \
            TIMESTAMP(); \
            printf(" [ERROR] %s:%d - %s\n", __FILE__, __LINE__, msg); \
        } while(0)
#else
    #define LOG_ERROR(msg)
#endif

#if DEBUG_LEVEL >= DEBUG_WARN
    #define LOG_WARN(msg) \
        do { \
            TIMESTAMP(); \
            printf(" [WARN] %s - %s\n", __func__, msg); \
        } while(0)
#else
    #define LOG_WARN(msg)
#endif

#if DEBUG_LEVEL >= DEBUG_INFO
    #define LOG_INFO(msg) \
        printf("[INFO] %s\n", msg)
#else
    #define LOG_INFO(msg)
#endif

// Value printing macro
#define PRINT_VAR(x) printf(#x " = %d\n", x)

// Assertion macro
#define ASSERT(cond) \
    do { \
        if (!(cond)) { \
            LOG_ERROR("Assertion failed: " #cond); \
            return -1; \
        } \
    } while(0)

int divide(int a, int b) {
    LOG_WARN("divide function called");
    PRINT_VAR(a);
    PRINT_VAR(b);
    
    ASSERT(b != 0);
    
    int result = a / b;
    LOG_INFO("Division successful");
    return result;
}

int main() {
    LOG_INFO("Program starting");
    
    int x = 10, y = 2;
    int z = divide(x, y);
    printf("Result: %d\n", z);
    
    LOG_WARN("Testing division by zero");
    z = divide(10, 0);  // This will trigger assertion
    
    return 0;
}