C

C Fundamentals

18 lessons

Progress0%
1. Introduction to C
1What is C?
2. Variables and Data Types
1Data Types in C
3. Control Flow
ConditionalsLoops
4. Functions
Defining FunctionsRecursion
5. Arrays and Pointers
Arrays and StringsPointers
6. Memory Management
Dynamic MemoryStructs and Files
7. Preprocessor & Macros
Preprocessor DirectivesMacros & Inline Functions
8. Bitwise Operations
Bitwise OperatorsBit Flags & Masking
9. Enums, Unions & typedef
Enums & typedefUnions & Complex Types
10. Multi-file Programs
Header Files & Compilation UnitsLinkage, Storage Classes & Make
All Tutorials
CPreprocessor & Macros
Lesson 11 of 18 min
Chapter 7 · Lesson 1

Preprocessor Directives

Preprocessor Directives

Before your C source file is compiled into machine code it passes through several distinct phases. Understanding these phases — especially the first one — is essential for writing well-structured C programs.

The Compilation Pipeline

  1. Preprocessing — the preprocessor reads your source file and handles all lines beginning with #. It performs text substitution, file inclusion, and conditional compilation. The output is a pure C translation unit with no # directives left.
  2. Compilation — the compiler translates the preprocessed C source into assembly or object code.
  3. Linking — the linker combines object files and resolves external references (functions from other files or libraries) into the final executable.

#include — File Inclusion

#include pastes the contents of another file in place:

  • #include <stdio.h> — angle brackets search the system/standard include directories first.
  • #include "myheader.h" — quotes search the current project directory first, then fall back to system directories.

Use angle brackets for standard library and third-party library headers. Use quotes for your own project headers.

#define — Symbolic Constants

#define NAME value creates a symbolic constant. The preprocessor replaces every occurrence of NAME with value before compilation begins. Unlike const variables, macros have no type and no scope:

c
#define MAX_SIZE 100

Use #undef NAME to remove a macro definition so a new one can be given.

Conditional Compilation

Conditional directives allow you to include or exclude code depending on whether a macro is defined or what value it holds. This is invaluable for cross-platform code and debug builds:

  • #ifdef NAME — true if NAME is defined (regardless of value).
  • #ifndef NAME — true if NAME is NOT defined. Classic use: include guards.
  • #if expression — evaluates a constant integer expression.
  • #elif expression — else-if branch.
  • #else — else branch.
  • #endif — closes the conditional block.

Include Guards

When a header is included in multiple source files (or indirectly included more than once), its contents would otherwise be pasted repeatedly, causing redefinition errors. Include guards prevent this:

c
#ifndef MY_HEADER_H
#define MY_HEADER_H
/* header content */
#endif

Many compilers also support #pragma once as a non-standard but widely accepted alternative. Using include guards is more portable; #pragma once is terser. Many modern projects use both.

Predefined Macros

The C standard guarantees several built-in macros that the preprocessor defines automatically:

MacroExpands to
__FILE__Current source file name (string literal)
__LINE__Current line number (integer)
__DATE__Compilation date as "Mmm dd yyyy"
__TIME__Compilation time as "hh:mm:ss"
__func__Current function name (C99, not a macro but behaves like one)

These are extremely useful for logging, assertions, and debugging without a debugger.

Code Examples

Include Guard Patternc
/* --- mathutils.h --- */
#ifndef MATHUTILS_H
#define MATHUTILS_H

/* Only compiled once even if included multiple times */
#define PI 3.14159265358979323846

double circle_area(double radius);
double circle_circumference(double radius);

#endif /* MATHUTILS_H */

/* --- mathutils.c --- */
#include "mathutils.h"   /* quotes for own headers */
#include <math.h>        /* angle brackets for system headers */

double circle_area(double radius) {
    return PI * radius * radius;
}

double circle_circumference(double radius) {
    return 2.0 * PI * radius;
}

/* --- main.c --- */
#include <stdio.h>
#include "mathutils.h"

int main(void) {
    printf("Area    = %.4f\n", circle_area(5.0));
    printf("Circum. = %.4f\n", circle_circumference(5.0));
    return 0;
}

The include guard (#ifndef/#define/#endif) ensures mathutils.h is processed only once per translation unit, preventing duplicate declarations. PI is defined once and shared across both files.

Conditional Compilation for Debug Buildsc
#include <stdio.h>

/* Compile with -DDEBUG to enable debug output:
   gcc -DDEBUG main.c -o main */

#ifdef DEBUG
  #define LOG(fmt, ...) fprintf(stderr, "[DEBUG %s:%d] " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__)
#else
  #define LOG(fmt, ...) /* nothing — stripped in release builds */
#endif

int divide(int a, int b) {
    LOG("divide called: a=%d, b=%d", a, b);
    if (b == 0) {
        fprintf(stderr, "Error: division by zero\n");
        return 0;
    }
    int result = a / b;
    LOG("result = %d", result);
    return result;
}

int main(void) {
    int x = divide(10, 2);
    int y = divide(7, 0);
    printf("x=%d  y=%d\n", x, y);
    return 0;
}

When compiled with -DDEBUG the LOG macro expands to fprintf with file and line info. In release builds LOG expands to nothing — zero overhead. The ##__VA_ARGS__ trick removes the trailing comma when no extra args are given.

Predefined Macrosc
#include <stdio.h>

void show_location(void) {
    /* __func__ is a C99 predefined identifier, behaves like a string macro */
    printf("Function : %s\n", __func__);
    printf("File     : %s\n", __FILE__);
    printf("Line     : %d\n", __LINE__);
    printf("Compiled : %s at %s\n", __DATE__, __TIME__);
}

/* Portable compile-time assertion using preprocessor */
#define STATIC_ASSERT(cond, msg) typedef char static_assert_##msg[(cond) ? 1 : -1]

STATIC_ASSERT(sizeof(int) >= 4, int_must_be_at_least_4_bytes);

int main(void) {
    show_location();

    /* Platform detection example */
#if defined(_WIN32)
    printf("Platform : Windows\n");
#elif defined(__linux__)
    printf("Platform : Linux\n");
#elif defined(__APPLE__)
    printf("Platform : macOS\n");
#else
    printf("Platform : Unknown\n");
#endif

    return 0;
}

__FILE__, __LINE__, __DATE__, __TIME__ are replaced at compile time. STATIC_ASSERT uses a trick: declaring an array of size -1 causes a compile error if the condition is false — giving you a compile-time check with zero runtime cost.

Quick Quiz

1. What is the purpose of an include guard (#ifndef / #define / #endif) in a header file?

2. What is the difference between #include <file.h> and #include "file.h"?

3. Which predefined macro gives the current source file's line number?

4. During which phase of compilation are #define macros processed?

Was this lesson helpful?

PreviousNext