Preprocessor directives

The C preprocessor scans and modifies the source code before it goes into compilation. It plays a crucial role in ensuring the code is ready for the compiler by handling various tasks. For example, comments in the source code are ignored by the C compiler, and they are removed during the preprocessing stage, before actual compilation begins. This allows the compiler to only work with the raw C code, without being distracted by the comments, which are there just for documentation and clarity for developers.

In the similar way, All Preprocessor directives in C are primarily used for code maintenance, conditional compilation, and including files. These directives are handled by the preprocessor and are not passed on to the compiler. Their purpose is to make the code more manageable, customizable, and adaptable for different environments or conditions, but they do not directly generate any machine code. In this chapter we are going to cover:

  1. Macros
  2. Nesting Macros
  3. Macros with parameters
  4. Macros with functions
  5. #undef
  6. File inclusion
  7. Conditional Compilation

Rules for Preprocessor Directives

Before diving into the preprocessor’s functions, let’s go over some basic rules:

  1. Preprocessor directives must start with a #.
  2. They should not end with a semicolon.
  3. To continue a directive onto the next line, the current line must end with a backslash \.
  4. Preprocessor directives can be written anywhere in the program.
1. Macros

A macro is a fragment of code that gets replaced by its value or definition during the preprocessing stage. Simple macro substitution is done using the #define directive. The syntax is:

#define MACRO_NAME replacement_text

When the preprocessor encounters MACRO_NAME in the code, it replaces it with replacement_text.

Example
#include <stdio.h>

#define PI 3.14

int main() {
    float radius = 5;
    float area = PI * radius * radius;
    printf("Area of the circle: %f\n", area);
    return 0;
}

In this code:

  • #define PI 3.14 defines a macro named PI.
  • Everywhere PI is used in the program, the preprocessor replaces it with 3.14 before compiling.
Key Points:
  1. No Data Type: A macro is not a variable and has no data type. It simply substitutes text.
  2. Global Replacement: Macros are replaced throughout the entire program.
2. Nesting in Macros

Nesting macros involves defining one macro inside another, allowing you to reuse logic or expressions.

Example

#include <stdio.h>

#define PI               3.14
#define PI_SQUARE         PI * PI

int main() {
    printf("square of PI is %d\n", PI_SQUARE);
    return 0;
}
Here, PI_SQUARE is a nested macro that uses PI to calculate the square of PI value.
3. Macros with Parameters

A macro with parameters works like a function but is expanded during preprocessing. The arguments passed to the macro are replaced directly into the code.

Example

#include <stdio.h>

#define BIT(a)           1<<a

int main() {
    printf("value: %d\n", BIT(7));
    return 0;
}

BIT is a macro that takes a as an argument, it left shifts 1 by a times.

4. Macros with Functions

Macros can act like functions but without the overhead of a function call. They simply replace code at compile time, often used for small operations where performance is crucial.

Example:

#include <stdio.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main() {
    int x = 10, y = 20;
    printf("Max: %d\n", MAX(x, y));
    return 0;
}

The macro MAX(a, b) is a function-like macro that compares two numbers and returns the maximum.

5. #undef

The #undef directive is used to undefine a macro that was previously defined using #define. Once undefined, the macro cannot be used unless redefined.

#include <stdio.h>

#define MAX_ARR_SIZE       50


int main() {
   printf("max array size is %d\n", MAX_ARR_SIZE);

#undef MAX_ARR_SIZE

    printf("max array size after undefined: %d\n", MAX_ARR_SIZE);
    return 0;
}

Error while compiling

 error: ‘MAX_ARR_SIZE’ undeclared (first use in this function)
   11 |     printf("max array size after undefined: %d\n", MAX_ARR_SIZE);
      |                                                    ^~~~~~~~~~~~

The macro can be redefined later if #undef is used

#include <stdio.h>

#define MAX_ARR_SIZE       50


int main() {
   printf("max array size is %d\n", MAX_ARR_SIZE);

#undef MAX_ARR_SIZE
#define MAX_ARR_SIZE         10

    printf("max array size after redefined: %d\n", MAX_ARR_SIZE);
    return 0;
}

output

max array size is 50
max array size after redefined: 10
5. File inclusions

#include is used to include header files or other files into your program. You can include standard or user-defined headers.

Example:

#include <stdio.h>  // Standard library inclusion
#include "myheader.h"  // User-defined header inclusion

int main() {
    printf("Hello World\n");
    return 0;
}

Files like <stdio.h> provide standard input/output functions.

"myheader.h" is a user-defined header that can contain custom macros or function declarations.

6. Conditional Compilation

Conditional compilation allows you to compile certain parts of code based on conditions. This is useful for platform-specific code or debugging.

#if and #endif

Compiles code only if the condition is true.

#include <stdio.h>

#define DEBUG 1

#if DEBUG
    #define LOG(x) printf("Log: %s\n", x)
#endif

int main() {
    LOG("Debugging is enabled");
    return 0;
}

If DEBUG is defined as 1, the LOG macro is included.

#if, #else, #endif

Provides an alternative if the condition is false

#include <stdio.h>

#define VERSION 2

#if VERSION == 1
    #define FEATURE "Version 1 features"
#else
    #define FEATURE "Version 2 features"
#endif

int main() {
    printf("%s\n", FEATURE);
    return 0;
}

Depending on the VERSION, different code will be compiled.

#if, #elif, #endif

Allows multiple conditions.

#include <stdio.h>

#define LEVEL 3

#if LEVEL == 1
    #define MODE "Low"
#elif LEVEL == 2
    #define MODE "Medium"
#else
    #define MODE "High"
#endif

int main() {
    printf("Mode: %s\n", MODE);
    return 0;
}

Different LEVEL values determine the mode.

#ifdef and #endif

Checks if a macro is defined.

#include <stdio.h>

#define FEATURE_ENABLED

#ifdef FEATURE_ENABLED
    #define MESSAGE "Feature is enabled"
#endif

int main() {
    printf("%s\n", MESSAGE);
    return 0;
}

MESSAGE is only defined if FEATURE_ENABLED is present.

#ifndef and #endif

Checks if a macro is not defined.

#include <stdio.h>

#ifndef MAX_SIZE
    #define MAX_SIZE 100
#endif

int main() {
    printf("Max Size: %d\n", MAX_SIZE);
    return 0;
}

MAX_SIZE is defined only if it hasn’t been defined before.

Summary

#define:
Used to define macros. A macro is a fragment of code that gets replaced throughout the code wherever it is referenced.

#if:
Used for conditional compilation. It allows compiling specific code only if a condition is true.

#else:
Works with #if or #ifdef to specify alternative code to be compiled if the previous condition is false.

#elif:
Stands for “else if.” It combines #else and #if to check another condition if the previous one is false.

#endif:
Closes an #if, #ifdef, or #ifndef block.

#include:
Used to include the contents of a file into the current file, typically used for including header files.

#ifdef:
Used to check if a macro is defined. If the macro is defined, the code inside the block will be compiled.

#ifndef:
The opposite of #ifdef. It checks if a macro is not defined.

#undef:
Used to undefine a macro, which cancels its previous definition.