C Programming

Module 6· 7 min read· 10 Questions·completed

6. Preprocessor, Macros & Type Qualifiers

Macro expansion traps, stringification (#), token pasting (##), include guards, do-while(0) idiom, const correctness variations, volatile, and restrict in C

GATE CS & UGC NET JRF Core Subject Exam Weightage: 2 Questions every year on macro argument precedence errors, multi-statement macro scoping, and the exact difference between const int *, int * const, and volatile.


1. Technical Jargon & The Compilation Pipeline#

Before the C compiler compiles code into assembly, the source file passes through the C Preprocessor (CPP), an automated textual substitution tool.

Diagram / Text
Source File (.c) ──> [ Preprocessor ] ──> Preprocessed Source (.i)
                            │
                      - Expands #include headers
                      - Replaces #define macros
                      - Strips comments
                      - Evaluates #ifdef / #if conditionals
  1. Tokenization: Breaking the character stream into discrete syntactic lexical tokens.
  2. Macro Expansion: Textually substituting a macro name with its replacement list.
  3. Stringification Operator (#): Converts a macro parameter into a string literal enclosed in double quotes.
  4. Token-Pasting / Concatenation Operator (##): Merges two separate lexical tokens into a single valid token during preprocessing.
  5. Include Guard: Preprocessor idiom preventing a header file from being included multiple times within the same translation unit.
  6. Type Qualifier: Keywords (const, volatile, restrict) that modify how the compiler accesses and optimizes memory.
  7. Strict Aliasing: The ISO C optimization rule stating that two pointers of different incompatible types cannot point to the same memory object (with few exceptions like char*).

2. Macro Pitfalls & Syntax Traps#

Macros are simple textual substitutions. They have no type checking, no scope, and no knowledge of C operator precedence!

2.1 The Missing Parentheses Trap#

C
// NAIVE MACRO (Dangerous!):
#define SQUARE(x) x * x

int main(void) {
    int res = SQUARE(2 + 3);
    // Expands textually to: 2 + 3 * 2 + 3
    // By precedence: 2 + (3 * 2) + 3 = 2 + 6 + 3 = 11!
    // Expected: (2 + 3)^2 = 25!
    printf("%d\n", res); // Prints 11
    return 0;
}
C
// CORRECT DEFENSIVE MACRO:
#define SQUARE(x) ((x) * (x))
// SQUARE(2 + 3) expands to: ((2 + 3) * (2 + 3)) = 25

2.2 The Side-Effect Argument Trap#

Even fully parenthesized macros fail when passed arguments containing side-effects (++, --):

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

int x = 5, y = 10;
int z = MAX(x++, y++);
// Expands to: ((x++) > (y++) ? (x++) : (y++))
// Since 5 > 10 is FALSE:
// y++ is evaluated TWICE!
// y becomes 12, z becomes 11, x becomes 6!

[!CRITICAL] GATE Rule: Never pass expressions with side effects (i++, function calls) to macros.


2.3 The Multi-Statement Macro Trap & do { ... } while(0)#

If a macro contains multiple statements, wrapping it in curly braces { ... } breaks inside if-else statements:

C
// BROKEN MACRO:
#define SWAP(a, b) { int temp = a; a = b; b = temp; }

if (condition)
    SWAP(x, y); // The trailing semicolon causes: { ... }; else
else            // SYNTAX ERROR: 'else' without a previous 'if'!
    foo();
C
// THE CANONICAL IDIOM: do { ... } while(0)
#define SWAP(a, b) do { int temp = a; a = b; b = temp; } while(0)

if (condition)
    SWAP(x, y); // Expands to: do { ... } while(0); else
else            // Syntactically PERFECT!
    foo();

3. Stringification (#) and Token-Pasting (##)#

C
#include <stdio.h>

// 1. Stringification (#): Converts parameter into "string literal"
#define PRINT_INT(x) printf(#x " = %d\n", x)

// 2. Token-Pasting (##): Merges two tokens into one variable name
#define MAKE_VAR(prefix, id) prefix##_##id

int main(void) {
    int score = 95;
    PRINT_INT(score); // Expands to: printf("score" " = %d
", score); Prints "score = 95"

    int MAKE_VAR(student, 101) = 42; // Expands to: int student_101 = 42;
    printf("%d\n", student_101);
    return 0;
}

4. Type Qualifiers: const, volatile, restrict#

4.1 Decoding the 3 const Pointer Variations#

Read the declaration backwards (from right to left) starting from the identifier:

Diagram / Text
1. const int *p;
   Read: 'p is a pointer to an integer that is CONSTANT.'
   - The data (*p) is read-only: *p = 20; (ERROR)
   - The pointer (p) can change:   p = &other; (LEGAL)

2. int * const p = &x;
   Read: 'p is a CONSTANT pointer to an integer.'
   - The pointer (p) is locked:    p = &other; (ERROR)
   - The data (*p) is mutable:    *p = 20; (LEGAL)

3. const int * const p = &x;
   Read: 'p is a CONSTANT pointer to a CONSTANT integer.'
   - Neither the address nor the data can be modified.

4.2 The volatile Qualifier#

The volatile keyword tells the compiler: "The value of this variable can change at any time through means outside the compiler's control."

C
// Hardware Memory-Mapped Register Example:
volatile unsigned int *status_reg = (unsigned int *)0x40001000;

// Without 'volatile', the compiler sees no writes to *status_reg in the loop
// and optimizes this into an INFINITE LOOP reading the register ONCE!
while (*status_reg == 0) {
    // Wait for hardware to set bit
}
// With 'volatile', the compiler forces a fresh RAM/Bus read on EVERY iteration!

When to Use volatile:#

  1. Memory-mapped I/O peripheral registers in embedded systems and device drivers.
  2. Global variables modified by Interrupt Service Routines (ISRs).
  3. Flags shared across threads in multithreading (though atomic types are preferred in C11).

4.3 The restrict Qualifier (C99)#

restrict is an optimization contract between the programmer and the compiler applying exclusively to pointers:

C
void vector_add(int * restrict a, int * restrict b, int * restrict c, int n) {
    for (int i = 0; i < n; i++) {
        a[i] = b[i] + c[i];
    }
}

By declaring a, b, and c as restrict, you guarantee to the compiler that arrays a, b, and c do not overlap in memory (no aliasing). This allows the compiler to aggressively vectorize loops and cache values in CPU registers without fear that writing to a[i] will overwrite b[i].


5. Best Practices & Defensive Habits#

  1. Prefer static inline Functions Over Complex Macros: Inline functions provide the exact same zero-overhead performance as macros while offering strict type checking, clean debugging, and zero side-effect double-evaluation hazards.
  2. Always Wrap Macro Definitions in Parentheses: Parenthesize both the whole expression ((...)) and each parameter usage ((x)).
  3. Use Standard Include Guards:
    C
    #ifndef MY_MODULE_H
    #define MY_MODULE_H
    // Declarations...
    #endif /* MY_MODULE_H */
    

Practice Quiz

Test your understanding with step-by-step solutions

Practice Quiz

10 questions · 90s per question

Each question has a 90-second time limit. Unanswered questions will be auto-submitted when time runs out.