Lecture 23 | Unit VI

Preprocessor & Directives, Macro, Macro vs Functions

C Programming for BTech First Semester

Instructor: Mohsin F. Dar

Designation: Assistant Professor

Department: Cloud & Software Operations Cluster | SOCS

University: UPES

What is a Preprocessor?

  • The preprocessor is a program that processes the source code before compilation
  • It performs textual substitution and file inclusion
  • All preprocessor directives begin with # symbol
  • Preprocessor directives are not C statements - they don't end with semicolon
  • The output of preprocessing is an expanded source code which is then passed to the compiler
#include <stdio.h>     // Preprocessor directive
#define PI 3.14159      // Preprocessor directive

int main() {
    printf("Value: %f", PI);  // PI will be replaced with 3.14159
    return 0;
}

Types of Preprocessor Directives

File Inclusion

  • #include

Macro Definition

  • #define
  • #undef

Conditional Compilation

  • #if, #elif, #else, #endif
  • #ifdef, #ifndef

Other Directives

  • #pragma
  • #error
  • #line

#include Directive

Purpose: File Inclusion

Used to include the contents of a file into the source code

Two Forms:

#include <filename.h>    // System header files (standard library)
#include "filename.h"    // User-defined header files
                    
Difference:
<filename.h> - Searches in system directories
"filename.h" - Searches in current directory first, then system directories
#include <stdio.h>      // Standard I/O
#include <stdlib.h>     // Standard library
#include "myheader.h"   // Custom header
                    

Creating User-defined Header Files

Example: math_utils.h

// math_utils.h - Header file with mathematical utilities
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// Function declarations
int add(int a, int b);
int subtract(int a, int b);
double calculate_circle_area(double radius);

// Constant definition
#define PI 3.14159

#endif // MATH_UTILS_H

math_utils.c - Implementation

#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

double calculate_circle_area(double radius) {
    return PI * radius * radius;
}
Key Points:
• Use #ifndef and #define for include guards
• Only put function declarations and macro definitions in header files
• Keep the implementation in corresponding .c files
• Include the header in both the implementation and files that use these functions

Using User-defined Headers

main.c - Using our math_utils.h

#include <stdio.h>
#include "math_utils.h"  // Note the quotes for user headers

int main() {
    int x = 10, y = 5;
    
    printf("%d + %d = %d\n", x, y, add(x, y));
    printf("%d - %d = %d\n", x, y, subtract(x, y));
    
    double radius = 3.0;
    printf("Area of circle with radius %.1f = %.2f\n", 
           radius, calculate_circle_area(radius));
    
    printf("Value of PI: %f\n", PI);
    
    return 0;
}

Compilation

# Compile all source files together
gcc main.c math_utils.c -o program

# Run the program
./program
Best Practices:
• Keep header files in the same directory as your source files
• Use descriptive names for header guards (e.g., MATH_UTILS_H)
• Document your header files with comments
• Group related functions in the same header file

#define Directive - Object-like Macros

Syntax:

#define MACRO_NAME replacement_text

Examples:

#define PI 3.14159
#define MAX_SIZE 100
#define NEWLINE '\n'
#define MESSAGE "Hello, World!"

int main() {
    int arr[MAX_SIZE];           // Becomes: int arr[100];
    double area = PI * r * r;  // PI replaced with 3.14159
    printf("%s%c", MESSAGE, NEWLINE);
    return 0;
}
                    
Note: Macro names are conventionally written in UPPERCASE to distinguish them from variables

#define Directive - Function-like Macros

Syntax:

#define MACRO_NAME(parameters) replacement_text

Examples:

#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define IS_EVEN(n) ((n) % 2 == 0)
#define ABS(x) ((x) < 0 ? -(x) : (x))

int main() {
    int num = 5;
    printf("Square: %d\n", SQUARE(num));      // ((5) * (5))
    printf("Max: %d\n", MAX(10, 20));         // ((10) > (20) ? (10) : (20))
    printf("Is Even: %d\n", IS_EVEN(num));   // ((5) % 2 == 0)
    return 0;
}
                    
Important: Use parentheses around parameters to avoid precedence issues!

Common Macro Pitfalls

1. Missing Parentheses Problem

// WRONG - No parentheses
#define SQUARE(x) x * x

int result = SQUARE(2 + 3);    // Expands to: 2 + 3 * 2 + 3 = 11 (WRONG!)

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

int result = SQUARE(2 + 3);    // Expands to: ((2 + 3) * (2 + 3)) = 25 (CORRECT!)
                    

2. Side Effects Problem

#define SQUARE(x) ((x) * (x))

int a = 5;
int result = SQUARE(a++);    // Expands to: ((a++) * (a++))
                                  // a gets incremented TWICE!
                    
Warning: Never use increment/decrement operators with macro arguments!

Conditional Compilation Directives

Purpose: Compile code conditionally

#define DEBUG 1

#ifdef DEBUG
    printf("Debug mode is ON\n");
#endif

#ifndef MAX_SIZE
    #define MAX_SIZE 100
#endif

#if DEBUG == 1
    printf("Debugging level 1\n");
#elif DEBUG == 2
    printf("Debugging level 2\n");
#else
    printf("No debugging\n");
#endif
                    
Use Cases:
• Platform-specific code
• Debug vs Release builds
• Feature toggling
• Header guard implementation

#undef and Predefined Macros

#undef - Undefine a Macro

#define TEMP 100
printf("%d", TEMP);    // Prints 100

#undef TEMP
#define TEMP 200
printf("%d", TEMP);    // Prints 200
                    

Standard Predefined Macros

printf("File: %s\n", __FILE__);        // Current filename
printf("Line: %d\n", __LINE__);        // Current line number
printf("Date: %s\n", __DATE__);        // Compilation date
printf("Time: %s\n", __TIME__);        // Compilation time
printf("ANSI: %d\n", __STDC__);        // 1 if ANSI C
                    

Macros vs Functions: Detailed Comparison

Aspect Macros Functions
Processing Processed by preprocessor (before compilation) Processed by compiler
Execution Code is expanded inline (no function call) Function call overhead exists
Speed Faster (no call overhead) Slightly slower (function call overhead)
Code Size Increases code size (code duplication) Code is reused (single copy)
Type Checking No type checking Strict type checking

Macros vs Functions: Comparison (Continued)

Aspect Macros Functions
Debugging Difficult to debug (expanded code) Easy to debug
Side Effects Prone to side effects (e.g., a++) No side effects issue
Recursion Not possible Possible
Memory No separate memory allocation Uses stack memory
Flexibility Works with any data type Type-specific

When to Use Macros vs Functions?

Use Macros When:

  • Simple operations needed
  • Performance is critical
  • Type-generic code required
  • Constants definition
  • Code must work with different types
  • Conditional compilation needed
#define MAX(a,b) ((a)>(b)?(a):(b))
#define PI 3.14159
#define SIZE 100
                            

Use Functions When:

  • Complex operations needed
  • Type safety is important
  • Debugging is required
  • Code reusability matters
  • Recursion is needed
  • Multiple statements involved
int factorial(int n) {
    if(n <= 1) return 1;
    return n * factorial(n-1);
}

Practical Example: Complete Program

#include <stdio.h>

// Object-like macros
#define PI 3.14159
#define MAX_SIZE 10

// Function-like macros
#define SQUARE(x) ((x) * (x))
#define CIRCLE_AREA(r) (PI * SQUARE(r))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

// Conditional compilation
#define DEBUG 1

int main() {
    int radius = 5;
    double area = CIRCLE_AREA(radius);
    
    printf("Area of circle with radius %d: %.2f\n", radius, area);
    printf("Square of %d: %d\n", radius, SQUARE(radius));
    printf("Max of %d and 10: %d\n\n", radius, MAX(radius, 10));
    
    #ifdef DEBUG
        printf("[DEBUG] File: %s, Line: %d\n", __FILE__, __LINE__);
        printf("[DEBUG] Compiled on: %s at %s\n", __DATE__, __TIME__);
    #endif
    
    return 0;
}

Summary

  • The preprocessor processes source code before compilation
  • #include is used for file inclusion
  • #define creates macros (object-like and function-like)
  • Macros are powerful but require careful handling to avoid pitfalls
  • Use parentheses in macro definitions to ensure correct evaluation
  • Conditional compilation allows platform-specific or debug code
  • Choose between macros and functions based on requirements
  • Standard predefined macros provide useful debugging information
Remember: Macros are not functions - they are text substitutions that happen before compilation!