BIT102 C Programming

C ProgrammingUnit 1010 min read

Preprocessor Directives & C Miscellany

Unit 10 of C Programming introduces preprocessor directives (macros, include, define), conditional compilation, and miscellaneous features like assert(), restrict, and static keyword nuances—essential for writing efficient, maintainable, and portable C code.

TAKEAWAYS:

  • Preprocessor directives (#define, #include, #ifdef) process code before compilation, enabling macros, modularity, and conditional compilation.
  • Macros (#define) create text substitutions, but unlike functions, they lack type-checking and scope—use sparingly for performance-critical code.
  • Header guards (#ifndef) prevent multiple inclusions of the same header file, avoiding redefinition errors.
  • assert() checks program invariants during debugging but is removed in release builds via #define NDEBUG.
  • restrict keyword hints to the compiler that pointers do not overlap, enabling optimizations like loop unrolling.
  • static in functions/files limits scope, while in variables ensures storage duration—critical for modularity and thread safety.

1. Preprocessor Basics: How It Works

The C preprocessor runs before compilation, transforming source code into a cleaner form. It handles directives prefixed with # (e.g., #include, #define). Unlike the compiler, it does no type-checking—macros are pure text replacements.

Key Preprocessor Directives

Directive Purpose Example
#include Inserts another file’s content (headers or source). #include <stdio.h>
#define Defines macros (text substitution) or constants. #define PI 3.14159
#undef Removes a macro definition. #undef MAX_SIZE
#ifdef/#ifndef Conditional compilation based on macro existence. #ifndef DEBUG #define DEBUG 1 #endif
#if/#elif/#else Evaluates expressions for conditional inclusion. #if DEBUG > 0 #include "debug_log.h" #endif
#line Overrides line/column numbers (useful for error messages). #line 100 "custom_file.c"

Visual: Preprocessor Flow

graph TD
    A["Source Code"] -->|"Preprocessor"| B["Macro Expansion"]
    B --> C["Header Inclusion"]
    C --> D["Conditional Compilation"]
    D --> E["Compiled Code"]

Why it matters: The preprocessor enables modularity (via #include) and portability (via #define for platform-specific code). For example:

  • eSewa’s payment gateway uses #ifdef to compile different encryption modules for Android/iOS.
  • NTC’s network scripts leverage #include to share common functions across routers.

2. Macros: Power and Pitfalls

Macros (#define) replace text during preprocessing. They are not functions—no type safety, no scope, and no evaluation order.

Types of Macros

  1. Object-like macros (constants):
    #define TRUE 1
    #define FALSE 0
    
  2. Function-like macros (with arguments):
    #define SQUARE(x) ((x)*(x))
    

Worked Example: Macro vs. Function

// Macro (no type-checking, evaluated twice)
#define SQUARE(x) (x)*(x)

// Function (type-checked, evaluated once)
int square(int x) { return x*x; }

Trace for SQUARE(3+1):

  • Macro: Expands to (3+1)*(3+1) → 16 (incorrect due to order of operations).
  • Function: Computes square(4) → 16 (correct).

Visual: Macro Expansion Macro: #define MAX(a,b) ((a)>(b)?(a):(b)) Before expansion:

int x = MAX(5+2, 3);

After expansion:

int x = ((5+2)>(3)?(5+2):(3));

Result: x = 7 (correct), but dangerous if arguments have side effects (e.g., MAX(f(), g()) calls f() twice).


Real-world use: Pathao’s ride-matching algorithm uses macros to define platform-specific constants (e.g., #define MAX_DRIVERS 1000) for quick compilation.


3. Header Files and Guards

Header files (*.h) declare functions/variables but avoid multiple inclusions via header guards.

Bad Practice (No Guards)

// math.h (included twice → redefinition error)
int add(int a, int b);

Good Practice (With Guards)

// math.h (safe inclusion)
#ifndef MATH_H
#define MATH_H
int add(int a, int b);
#endif

Visual: Header Inclusion Flow

graph TD
    A["#include 'math.h'"] --> B["Check #ifndef MATH_H"]
    B -->|"False"| C["Skip (already included)"]
    B -->|"True"| D["Define MATH_H & include"]

Why it matters: NEPSE’s trading scripts use header guards to share common functions (e.g., order_validation.h) without duplication.


4. Conditional Compilation

Compile different code paths based on macros or compiler flags.

Example: Debug vs. Release Builds

#define DEBUG 1

int main() {
    #if DEBUG
        printf("Debug mode enabled\n");
    #else
        printf("Release mode\n");
    #endif
    return 0;
}

Compile with -DDEBUG=0 to skip debug code.

Real-world use: Khalti’s payment SDK uses #ifdef MOBILE_PLATFORM to include platform-specific APIs.


5. assert() for Debugging

The assert() macro checks invariants during development (disabled in release builds via NDEBUG).

Syntax

#include <assert.h>
assert(condition); // Terminates if false

Example

int divide(int a, int b) {
    assert(b != 0); // Crash if b=0
    return a / b;
}

Compile with -DNDEBUG to remove all assert() calls.

Why it matters: NTC’s router firmware uses assert() to validate packet sizes before processing.


6. restrict Keyword

Hints to the compiler that pointers do not overlap, enabling optimizations like loop unrolling.

Example

void copy_array(int *restrict dest, const int *restrict src, int n) {
    for (int i = 0; i < n; i++) {
        dest[i] = src[i]; // Compiler assumes no overlap
    }
}

Advantage: The compiler may unroll loops or use SIMD instructions for speed.

Real-world use: Google’s TensorFlow uses restrict in memory-intensive operations (e.g., matrix multiplication).


7. static Keyword Deep Dive

Scope static in Function static in Variable static in File
Lifetime Function scope Program lifetime File scope
Linkage Internal Internal Internal
Example static int x = 5; static int y; static void foo()

Worked Example: File-Scoped static

// math_utils.c
static int hidden_counter = 0; // Only visible in this file
void increment() { hidden_counter++; }
// main.c
#include "math_utils.h"
int main() {
    increment(); // Works
    // hidden_counter++; // Error: not visible
    return 0;
}

Why it matters: Banks’ transaction modules use static to hide internal counters (e.g., static int last_tx_id = 0).


8. Miscellaneous Topics

(a) sizeof Operator

Returns size of a type or variable in bytes.

int a = 5;
printf("%zu\n", sizeof(a)); // 4 (on 32-bit systems)
printf("%zu\n", sizeof(int)); // 4

Use case: Daraz’s inventory system uses sizeof to calculate buffer sizes for product data.

(b) volatile Keyword

Prevents compiler optimizations for memory-mapped hardware (e.g., sensors).

volatile int *sensor_data = (int*)0x40000000; // Hardware register

Real-world use: Ncell’s signal strength monitors mark registers as volatile to ensure real-time updates.

(c) inline Functions

Suggests the compiler to replace calls with the function body (no overhead).

inline int square(int x) { return x*x; }

Limit: Only works for small, simple functions.


Comparison Table: Macros vs. Functions

Feature Macro (#define) Function
Type Safety ❌ No ✅ Yes
Scope ❌ Global ✅ Local
Evaluation Order ❌ Expands arguments twice ✅ Evaluates once
Overhead ❌ None ✅ Function call overhead
Example #define SQUARE(x) (x)*(x) int square(int x) { ... }

In the Real World

  1. eSewa’s Payment Gateway: Uses #define MAX_TRANSACTIONS 1000 to set a hard limit for concurrent payments, avoiding memory leaks. Conditional compilation (#ifdef MOBILE_APP) includes platform-specific APIs for Android/iOS.

  2. NEPSE’s Trading System: Employs assert() to validate market data (e.g., assert(price > 0)) during development. Header guards prevent duplicate inclusions of order_validation.h across modules.

  3. Pathao’s Ride Matching: Leverages static to hide internal counters (e.g., static int last_request_id) from other modules, ensuring thread safety in high-concurrency scenarios.


Exam Tip

  1. Preprocessor Directives:

    • Know the difference between #define (macro) and const (compile-time constant).
    • Common mistake: Forgetting #ifndef guards in headers → redefinition errors.
    • Marks: 1–2 questions on macro expansion or header inclusion.
  2. assert() and static:

    • assert() is debug-only (disabled with -DNDEBUG).
    • static in functions/variables limits scope—critical for modularity.
    • Marks: 1 question on static scope or assert() usage.
  3. restrict and volatile:

    • restrict is compiler optimization hint (rarely tested but high impact).
    • volatile is for hardware registers (e.g., sensors).
    • Marks: 1 short-answer question on restrict usage.
  4. Practical Focus:

    • Code snippets: Expect 1–2 questions on macro expansion or header guards.
    • Debugging: Given a snippet with assert() or static, identify the purpose.
    • Real-world tie: Link macros to apps like eSewa/Khalti (e.g., "How does #define help in payment processing?").

Final Visual: Preprocessor + Compiler Flow

graph TD
    A["Source Code"] --> B["Preprocessor"]
    B --> C["Macro Expansion<br/>Header Inclusion"]
    C --> D["Compiler"]
    D --> E["Assembler"]
    E --> F["Linker"]
    F --> G["Executable"]

Based on the TU BIT syllabus for C Programming (BIT102), unit 10.

Discussion

Loading…