Preprocessor Directives, Macros and Type Casting in C

Preprocessor Directives

The C preprocessor is a tool that runs before the actual compilation begins. It processes special instructions called directives — all of which begin with the # symbol. Directives are not C statements; they do not end with a semicolon and are processed before the compiler sees the code.

The preprocessor handles tasks like including files, defining constants and macros, and conditionally compiling sections of code.

Preprocessor Processing Flow


Source Code (.c)
     ↓
Preprocessor  ← handles all # directives
     ↓
Expanded Code  ← passed to compiler
     ↓
Compiler

1. #include — File Inclusion

Includes another file's contents into the current file before compilation.


#include <stdio.h>     // system header — search in compiler's include path
#include "myheader.h"   // user header  — search in current directory first

2. #define — Macros and Constants

#define creates a macro. The preprocessor replaces every occurrence of the macro name with its defined value before compilation. There is no type, no memory allocated, and no semicolon at the end.

Object-Like Macros (Constants)


#define PI        3.14159
#define MAX_SIZE  100
#define TRUE      1
#define FALSE     0

Function-Like Macros

Macros can take parameters, behaving like inline functions but without the overhead of a function call.


#define SQUARE(x)   ((x) * (x))
#define MAX(a, b)   ((a) > (b) ? (a) : (b))
#define MIN(a, b)   ((a) < (b) ? (a) : (b))
#define ABS(x)      ((x) < 0 ? -(x) : (x))

#include <stdio.h>

#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main() {
    int n = 5;
    printf("Square of %d = %d\n", n, SQUARE(n));    // 25
    printf("Square of 3+2 = %d\n", SQUARE(3 + 2));  // 25 (correct with parentheses)
    printf("Max of 8, 13  = %d\n", MAX(8, 13));      // 13
    return 0;
}

Output


Square of 5 = 25
Square of 3+2 = 25
Max of 8, 13  = 13

Important: Always wrap macro parameters in parentheses to avoid operator precedence bugs:


#define BAD_SQ(x)  x * x         // WRONG: BAD_SQ(3+2) → 3+2*3+2 = 11
#define GOOD_SQ(x) ((x) * (x))   // CORRECT: GOOD_SQ(3+2) → ((3+2)*(3+2)) = 25

#undef — Remove a Macro


#define LIMIT 100
// ... use LIMIT ...
#undef LIMIT      // LIMIT is no longer defined after this line

3. Conditional Compilation

Conditional compilation includes or excludes parts of code based on whether a macro is defined or what its value is. This is widely used for:

  • Enabling debug output during development
  • Writing platform-specific code
  • Enabling/disabling features without deleting code

#ifdef / #ifndef / #endif


#include <stdio.h>

#define DEBUG    // comment this out to disable debug output

int main() {
    int x = 42;

    #ifdef DEBUG
        printf("[DEBUG] x = %d\n", x);   // only compiled if DEBUG is defined
    #endif

    printf("Result: %d\n", x * 2);
    return 0;
}

Output (with DEBUG defined)


[DEBUG] x = 42
Result: 84

#if / #elif / #else / #endif


#define VERSION 2

#if VERSION == 1
    #define WELCOME "Welcome to Version 1"
#elif VERSION == 2
    #define WELCOME "Welcome to Version 2"
#else
    #define WELCOME "Welcome to Unknown Version"
#endif

#include <stdio.h>
int main() {
    printf("%s\n", WELCOME);
    return 0;
}

Output


Welcome to Version 2

#ifndef for Include Guards


#ifndef MY_HEADER_H
#define MY_HEADER_H
// ... header content ...
#endif

4. Predefined Macros

C compilers provide built-in macros that give useful information:

MacroValueDescription
__FILE__"main.c"Current file name
__LINE__42Current line number
__DATE__"Jan 01 2025"Compilation date
__TIME__"10:30:00"Compilation time
__STDC__1Standard C compiler (1 if ANSI C)

#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__);
    return 0;
}

Output


File : main.c
Line : 5
Date : Mar 10 2026
Time : 14:22:35

5. #pragma — Compiler Instructions


#pragma once             // include this header file only once (modern guard)
#pragma comment(lib, "ws2_32.lib")  // link a library on Windows

The goto Statement

The goto statement transfers control unconditionally to a labeled statement elsewhere in the same function. It is one of the oldest flow-control tools in C.

Syntax


goto label_name;
// ...
label_name:
    // code here is jumped to

Example — goto for Jumping Forward


#include <stdio.h>

int main() {
    int num;
    printf("Enter a positive number: ");
    scanf("%d", &num);

    if (num <= 0) {
        printf("Invalid input!\n");
        goto end;      // jump to 'end' label
    }

    printf("Square of %d = %d\n", num, num * num);

end:
    printf("Program finished.\n");
    return 0;
}

Sample Output


Enter a positive number: -5
Invalid input!
Program finished.

Practical Use — Exiting Nested Loops

Breaking out of deeply nested loops is one of the few accepted uses of goto:


#include <stdio.h>

int main() {
    int found = 0;

    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            printf("Checking [%d][%d]\n", i, j);
            if (i == 2 && j == 1) {
                printf("Found target at [%d][%d]!\n", i, j);
                goto done;  // exit both loops immediately
            }
        }
    }

done:
    printf("Search complete.\n");
    return 0;
}

Output


Checking [0][0]
...
Checking [2][1]
Found target at [2][1]!
Search complete.

Note on goto: While goto is valid C and occasionally useful (e.g., nested loop exits or cleanup in error handling), overusing it leads to "spaghetti code" that is hard to follow. In most cases, structured alternatives like break, return, or a flag variable are preferable.

Type Casting in C

Type casting converts a value from one data type to another. C supports two kinds of type conversion:

1. Implicit (Automatic) Type Conversion

The compiler automatically converts a lower type to a higher type to avoid data loss. This is called widening.


int   a = 10;
float b = a;    // int is automatically promoted to float
printf("%.1f\n", b);  // 10.0

Type Promotion Hierarchy


char → short → int → long → float → double → long double
         (lower)                              (higher)

2. Explicit Type Conversion (Casting)

The programmer forces a conversion using a cast operator (type).


(target_type) expression

Example — Integer Division vs Float Division


#include <stdio.h>

int main() {
    int a = 7, b = 2;

    printf("Integer division : %d\n",   a / b);              // 3 (truncates)
    printf("Float division   : %.2f\n", (float)a / b);       // 3.50
    printf("Float division   : %.2f\n", a / (float)b);       // 3.50

    return 0;
}

Output


Integer division : 3
Float division   : 3.50
Float division   : 3.50

Casting Between int and char


#include <stdio.h>

int main() {
    char ch = 'A';
    int  code = (int)ch;          // char to int
    char back = (char)(code + 1); // int back to char

    printf("'A' as int   : %d\n", code); // 65
    printf("65+1 as char : %c\n", back); // B

    return 0;
}

Output


'A' as int   : 65
65+1 as char : B

Casting in Expressions


#include <stdio.h>

int main() {
    int total = 250;
    int count = 7;
    float avg;

    // Without cast: integer division, loses decimal
    avg = total / count;
    printf("Without cast: %.4f\n", avg);  // 35.0000

    // With cast: correct decimal result
    avg = (float)total / count;
    printf("With cast   : %.4f\n", avg);  // 35.7143

    return 0;
}

Output


Without cast: 35.0000
With cast   : 35.7143

Casting Pointers


#include <stdio.h>

int main() {
    int  num = 65;
    char *cptr = (char *)&num;  // treat int memory as char bytes

    printf("int value : %d\n", num);
    printf("First byte as char: %c\n", *cptr);  // 'A' (ASCII 65)

    return 0;
}

Implicit vs Explicit Conversion Summary

FeatureImplicitExplicit (Cast)
Done byCompiler automaticallyProgrammer manually
DirectionLower → Higher type onlyAny direction
Data loss riskNone (widening)Possible (narrowing)
SyntaxNo syntax needed(target_type) value
Exampleint → float(int) 3.99 → 3

Summary

The C preprocessor runs before the compiler and handles file inclusion, macro substitution, conditional compilation, and pragma directives. Macros defined with #define can create constants and inline function-like substitutions — always use parentheses around parameters. Conditional compilation with #ifdef and #if enables flexible builds for different environments. The goto statement unconditionally jumps to a label — useful for exiting nested loops but should be used sparingly. Type casting converts values between data types, and explicit casting with (type) gives precise control over conversions that the compiler would otherwise refuse or do incorrectly.

Leave a Comment

Your email address will not be published. Required fields are marked *