C preprocessor, C macros, #include, #define, conditional compilation, #ifdef, #ifndef, header guards, debug macros, C programming in Tamil style, Tamil Technicians C course

Preprocessor & Macros in C – #include, #define, Conditional Compilation

Preprocessor & Macros in C – #include, #define, Conditional Compilation | Tamil Technicians

Tamil Technicians – C Programming Course · Preprocessor & Macros

Preprocessor & Macros in C – #include, #define, Conditional Compilation

When you compile a C program, the compiler is not the first thing that runs. Before that, a tool called the preprocessor processes your source code: it handles #include, #define and conditional compilation like #if, #ifdef, #ifndef.

In this lesson you will learn:

  • What the C preprocessor does before compilation.
  • How #include works with header files.
  • How to create and use #define macros (constants and function-like macros).
  • How to use conditional compilation (#if, #ifdef, #ifndef, #elif, #endif).
  • Common pitfalls with macros and best practices (when to prefer const or inline).
  • Technician-style examples: debug logs, hardware configuration, building for different boards.
C preprocessor and macros – include, define and conditional compilation visual diagram
Figure: The C preprocessor expanding macros, including header files, and enabling conditional compilation before the actual compilation step.

1. What Is the C Preprocessor?

The C preprocessor runs before the compiler. It reads your source code and processes all lines starting with # (called preprocessor directives).

Directive

#include

Inserts the contents of another file (usually header files) into your source code.

Directive

#define

Defines macros: symbolic constants or code patterns which are textually substituted.

Directive

#if / #ifdef

Enables or disables parts of code depending on conditions (conditional compilation).

Directive

#undef / others

Removes macro definitions or adds special instructions to the compiler.

Key idea

The preprocessor does textual substitution. It does not understand C types or semantics like the compiler does. It just follows the rules for macros and directives.

2. #include – Bringing in Declarations

You have seen lines like:

#include <stdio.h>
#include <stdlib.h>

These tell the preprocessor to copy the contents of the header files (stdio.h, stdlib.h) into your source file at that point.

Two forms of #include

Form Example Search path
#include <...> #include <stdio.h> Searches system include directories (standard library headers).
#include "..." #include "myutils.h" Searches current project folder first, then system paths (for user-defined headers).

Example: user-defined header

sensor.h

#ifndef SENSOR_H
#define SENSOR_H

float read_voltage(void);
float read_current(void);

#endif // SENSOR_H

main.c

#include <stdio.h>
#include "sensor.h"

int main() {
    printf("V = %.1f, I = %.2f\\n", read_voltage(), read_current());
    return 0;
}

The include guard pattern (#ifndef, #define, #endif) prevents the same header from being included multiple times in one compilation unit.

3. #define – Symbolic Constants (Object-like Macros)

The simplest use of #define is to define constants:

#define PI 3.14159
#define MAX_STUDENTS 100
#define SITE_VOLTAGE 415

Whenever the preprocessor sees PI in your code, it replaces it with 3.14159 (simple text substitution).

Technician-style example: configuration constants

#define MAX_INVERTERS 34
#define NOMINAL_AC_VOLTAGE 230.0
#define LOG_FILE "site_log.txt"

Then in code:

float voltage[MAX_INVERTERS];
printf("Nominal voltage: %.1f V\\n", NOMINAL_AC_VOLTAGE);
fp = fopen(LOG_FILE, "a");
In modern C, many programmers prefer const variables over #define for simple constants, because const respects type checking and debugger visibility:
const float NOMINAL_AC_VOLTAGE = 230.0f;

4. Function-like Macros – Small Inline Patterns

Macros can also take parameters (like functions), but they are still text-substitution:

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

Usage:

int a = 5;
int s = SQUARE(a + 1);   // expands to ((a + 1) * (a + 1))

Why use extra parentheses?

Without parentheses:

#define BAD_SQUARE(x) x * x
int s = BAD_SQUARE(a + 1);   // becomes a + 1 * a + 1 (wrong)

Using ((x) * (x)) ensures correct order of operations.

Technician-style macro examples

#define VOLT_TO_KW(v, i)   ((v) * (i) / 1000.0f)
#define IS_OVER_LIMIT(v, limit)  ((v) > (limit))

In code:

float v = 230.0f, i = 12.5f;
float power = VOLT_TO_KW(v, i);  // 2.875 kW approx

if (IS_OVER_LIMIT(v, NOMINAL_AC_VOLTAGE + 10.0f)) {
    printf("Voltage over limit!\\n");
}
Macros do not respect types or scopes like functions do. They are expanded before compilation. For complex logic, prefer real functions or inline functions (in C99 and later).

5. Macro Pitfalls & Best Practices

Pitfall Example Safer version
Missing parentheses #define MUL(a,b) a*b #define MUL(a,b) ((a)*(b))
Side effects in arguments #define SQR(x) ((x)*(x))
SQR(i++)
Don’t pass expressions with ++/-- into macros.
No type checking #define ADD(a,b) ((a)+(b)) Use real functions if types matter.
Unexpected scope Macros are global once defined. Use #undef or limit usage region.
Best practice
  • Use ALL_CAPS names for macros to distinguish them from variables and functions.
  • Add parentheses around macro parameters and the whole macro expression.
  • Prefer const variables and inline functions for complex logic.
  • Use macros mainly for: constants, small simple expressions, conditional compilation flags.

6. Conditional Compilation – #if, #ifdef, #ifndef, #elif, #endif

Conditional compilation lets you enable or disable blocks of code at compile time based on conditions. This is extremely useful for:

  • Debug vs release builds (extra logs, assertions).
  • Different hardware boards or sites.
  • Enabling or disabling experimental features.

Basic forms

#if condition
    // code
#elif other_condition
    // other code
#else
    // fallback
#endif
#ifdef MACRO_NAME
    // code if MACRO_NAME is defined
#endif

#ifndef MACRO_NAME
    // code if MACRO_NAME is NOT defined
#endif

Example: debug logging macro

#define DEBUG   // comment this line to disable debug

#ifdef DEBUG
    #define LOG(msg)  printf("DEBUG: %s\\n", msg)
#else
    #define LOG(msg)  // expands to nothing
#endif

int main() {
    LOG("Program started");
    // ...
    LOG("Program finished");
    return 0;
}

If DEBUG is defined, LOG("...") prints messages. If not, the macro expands to nothing and the debug code is removed from the compiled binary.

Technician-style example: site configuration

#define SITE_A
// #define SITE_B

#ifdef SITE_A
    #define SITE_NAME   "Nandhavanapatti"
    #define MAX_MW      10
#elif defined(SITE_B)
    #define SITE_NAME   "Another Solar Site"
    #define MAX_MW      5
#else
    #define SITE_NAME   "Unknown Site"
    #define MAX_MW      1
#endif

printf("Running config for %s (%d MW)\\n", SITE_NAME, MAX_MW);

7. Header Guards & #pragma once

If a header file is included multiple times (directly or indirectly), you can get duplicate-definition errors. To prevent this, we use header guards.

Standard pattern (portable)

#ifndef CONFIG_H
#define CONFIG_H

#define FW_VERSION "1.0.3"
#define MAX_INVERTERS 34

#endif // CONFIG_H

Here, if CONFIG_H is not yet defined, the block is included and CONFIG_H is defined. On subsequent includes, CONFIG_H is already defined so the content is skipped.

#pragma once (non-standard but common)

#pragma once

#define FW_VERSION "1.0.3"
#define MAX_INVERTERS 34

#pragma once tells the compiler to include the file only once. It is widely supported but not part of the C standard; header guards are fully portable.

8. #undef and Other Useful Directives

#undef – Cancel a Macro

#define TEMP_LIMIT 60

// ... some code

#undef TEMP_LIMIT   // macro is no longer defined here

After #undef TEMP_LIMIT, the name TEMP_LIMIT no longer expands as a macro.

#error – Generate a Compile-Time Error

#ifndef SITE_NAME
    #error "SITE_NAME is not defined! Please set site configuration."
#endif

This is useful to catch missing configuration at compile time instead of discovering it in runtime.

9. Real Use Cases – Technicians & Embedded C Style

9.1 Board-specific code

#define BOARD_V1
// #define BOARD_V2

#ifdef BOARD_V1
    #define RELAY_PIN 5
#elif defined(BOARD_V2)
    #define RELAY_PIN 12
#else
    #define RELAY_PIN 0
#endif

Now the rest of the code can use RELAY_PIN without if–else everywhere.

9.2 Enabling extra diagnostics only in development builds

#ifdef DEBUG
    #define DIAG_PRINT(fmt, ...) \
        printf("DIAG: " fmt "\\n", ##__VA_ARGS__)
#else
    #define DIAG_PRINT(fmt, ...)  // nothing
#endif

In production, you simply compile without DEBUG defined and your binaries are smaller and faster.

10. Learning Checklist – Preprocessor & Macros

  • I know that the preprocessor runs before the actual C compilation.
  • I understand how #include brings in declarations from header files.
  • I can use #define for symbolic constants and simple macros.
  • I know the difference between object-like macros and function-like macros.
  • I can write safe macros with proper parentheses and without side-effect arguments.
  • I can use #if, #ifdef, #ifndef, #elif, #endif for conditional compilation.
  • I know how header guards work and why they are important.
  • I understand when to prefer const or functions instead of complex macros.

FAQ: Preprocessor & Macros in C

1. Is the preprocessor part of the C compiler?

It is usually implemented as a separate step before compilation, although many modern compilers integrate it internally. Conceptually, it is a distinct phase that processes directives like #include and #define before normal C compilation begins.

2. When should I use macros instead of functions?

Macros are useful for simple constants, small inline expressions, and conditional compilation flags. For most calculations and logic, real functions (possibly marked inline) are safer because they have type checking, proper scoping and easier debugging.

3. Why do we need header guards?

Without header guards, including the same header multiple times can lead to duplicate declarations and definitions, causing compilation errors. Header guards ensure that a header’s contents are only included once in each translation unit.

4. What is the difference between #ifdef and #if defined()?

#ifdef MACRO is shorthand for #if defined(MACRO). Both check whether a macro is defined. #if defined(MACRO) || defined(OTHER) is more flexible when combining multiple conditions, but #ifdef is convenient for simple single-macro checks.

Category: C Programming Course · Lesson – Preprocessor & Macros in C (#include, #define, Conditional Compilation)


Buy link ;

Dell 15, 13th Generation Intel Core i5-1334U Processor, 16GB RAM, 1TB SSD, FHD 15.6″/39.62 cm Display, Windows 11, MSO’24, Silver, 1.62kg, Backlit Keyboard, 15 Month McAfee, Thin & Light Laptop.


Cantact link ;

Click Here >>>>


Leave a Comment

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

Scroll to Top