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:
| Macro | Value | Description |
|---|---|---|
| __FILE__ | "main.c" | Current file name |
| __LINE__ | 42 | Current line number |
| __DATE__ | "Jan 01 2025" | Compilation date |
| __TIME__ | "10:30:00" | Compilation time |
| __STDC__ | 1 | Standard 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 *)# // 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
| Feature | Implicit | Explicit (Cast) |
|---|---|---|
| Done by | Compiler automatically | Programmer manually |
| Direction | Lower → Higher type only | Any direction |
| Data loss risk | None (widening) | Possible (narrowing) |
| Syntax | No syntax needed | (target_type) value |
| Example | int → 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.
