Skip to main content

Advanced Topics

Enum, Typedef & Preprocessor

Master Enum, Typedef and the C Preprocessor — macros, file inclusion, conditional compilation, #error, #line, #pragma, stringizing and token pasting, variadic macros, include guards, platform detection, and predefined macros — using LEARNCODEGURU’s 6-level learning model.

Level 1 Basics Content

Enum, Typedef & the C Preprocessor — Overview

Before the compiler sees a single line of your program, the preprocessor rewrites it: pasting in headers, replacing macros, and deleting code that does not apply. This level starts with enum and typedef — two ways to give names to values and types — and then covers the preprocessor from #define and #include through conditional compilation, #error, #line, #pragma, stringizing and token pasting, variadic macros, include guards, platform detection, and the predefined macros.

Topic 1

What is an enum?

An enumeration is a user-defined type whose values are a fixed list of named integer constants. It replaces magic numbers with readable names.


enum Color { RED, GREEN, BLUE };   /* 0, 1, 2 */

enum Color c = GREEN;
printf("%d\n", c);              /* prints 1 */

Numbering rules:


enum A { X, Y, Z };              /* 0, 1, 2            */
enum B { P = 5, Q, R };          /* 5, 6, 7            */
enum C { M = 1, N = 10, O };    /* 1, 10, 11          */
enum D { S = 2, T = 2 };        /* duplicates allowed */
# Rule Detail
1 Underlying type Enum constants are of type int
2 Default numbering Starts at 0 and increases by 1
3 Explicit values Allowed; later members continue from the last value
4 Duplicates Permitted — two names may share a value
5 Scope of the names The constants are visible in the enclosing scope, not inside the enum
6 Range checking None — any int can be assigned
7 Printing Prints as a number; use an array of strings for names
Topic 2

Enum use — why and where

  • Readable constants — if (s == ERROR) beats if (s == 3)
  • Automatic numbering — add a member without renumbering by hand
  • Grouping — related values live together as one named type
  • Switch statements — the compiler can warn about unhandled members
  • State machines — states get names instead of numbers
  • Flags — with explicit powers of two you can OR them together
  • Debugger friendly — unlike a macro, an enum is a real symbol

/* State machine with a switch */
enum State { IDLE, RUNNING, PAUSED, STOPPED };

enum State s = RUNNING;

switch (s) {
    case IDLE:    puts("idle");    break;
    case RUNNING: puts("running"); break;
    case PAUSED:  puts("paused");  break;
    case STOPPED: puts("stopped"); break;
}

/* Flags — explicit powers of two can be combined */
enum Perm { READ = 1, WRITE = 2, EXEC = 4 };

int p = READ | WRITE;
if (p & WRITE) puts("writable");

/* Printing enum names — parallel string array */
enum Day { MON, TUE, WED, COUNT };

const char *names[] = { "Mon", "Tue", "Wed" };

for (int i = MON; i < COUNT; i++)
    printf("%d = %s\n", i, names[i]);
Aspect enum #define const int
Handled by Compiler Preprocessor Compiler
Has a type Yes (int) No Yes
Respects scope Yes No Yes
Visible in debugger Yes No Yes
Auto numbering Yes No No
Usable as array size Yes Yes Not in C89
Usable in case labels Yes Yes No
Topic 3

Typedef use

typedef gives an existing type a new name. It creates an alias, not a new type.


typedef unsigned long ulong;
ulong count = 0;

/* Structures — no need to repeat 'struct' */
typedef struct {
    char name[50];
    int  roll;
} Student;

Student s;                 /* instead of struct Student s; */

/* Enums */
typedef enum { FALSE, TRUE } Bool;
Bool done = FALSE;

/* Pointer types */
typedef char *String;
String msg = "hello";

/* Function pointers — the biggest readability win */
typedef int (*Compare)(const void *, const void *);
Compare cmp = myCompare;

/* Arrays */
typedef int Matrix[3][3];
Matrix m;                 /* int m[3][3]; */

Why use it:

  • Readability — the declaration describes the problem, not the plumbing
  • Shorter code — drop the repeated struct and enum keywords
  • Portability — change one typedef to switch int to long everywhere
  • Complex declarations — function pointers and arrays become readable names
  • Self-documenting — typedef int Celsius; states the unit
Aspect typedef #define
Processed by Compiler Preprocessor
Respects scope Yes No
Ends with Semicolon Newline (no semicolon)
Can alias complex types Yes — arrays, function pointers Only by fragile text substitution
typedef char *P; P a, b; Both are pointers #define P char * makes b a plain char
That last row is the classic exam question. With #define P char *, the line P a, b; becomes char *a, b; — only a is a pointer. A typedef makes both pointers because it aliases the type, not the text.
Topic 4.1

Introduction to the preprocessor

The preprocessor is a text-processing step that runs before compilation. It obeys lines beginning with # and produces an expanded source file called a translation unit.

Source .c → Preprocessor → Expanded source → Compiler → Assembly → Assembler → Object → Linker → Executable
# Aspect Preprocessor Compiler
1 Works on Plain text C language constructs
2 Understands types No Yes
3 Understands scope No Yes
4 Directives #define, #include, #if… None
5 Type checking None Full
6 Output Expanded source Object code
7 See it yourself gcc -E file.c gcc -c file.c
A directive occupies its own line, starts with #, and ends at the newline — not at a semicolon. Use a backslash at the end of a line to continue a long directive.
Topic 4.2

Macro definitions

Object-like macros — symbolic constants:


#define PI      3.14159
#define MAX     100
#define GREET   "Hello"

int a[MAX];                 /* becomes int a[100]; */

Function-like macros — with parameters:


#define SQUARE(x)   ((x) * (x))
#define MAX2(a, b)  (((a) > (b)) ? (a) : (b))

printf("%d\n", SQUARE(2 + 3));   /* 25 — parentheses saved us */

/* Why the parentheses matter */
#define BAD(x)  x * x

BAD(2 + 3)   /* expands to 2 + 3 * 2 + 3  = 11, not 25 */

Multi-statement macros — the do while (0) idiom:


#define SWAP(a, b) do {        \
    int tmp = (a);              \
    (a) = (b);                   \
    (b) = tmp;                   \
} while (0)

SWAP(x, y);   /* behaves like one statement */

Undefining — #undef:


#define SIZE 10
int a[SIZE];

#undef SIZE          /* SIZE no longer defined */
#define SIZE 20       /* safe to redefine now  */

Macros vs functions:

# Aspect Macro Function
1 Handled by Preprocessor Compiler
2 Mechanism Text substitution Actual call
3 Speed No call overhead Small call overhead
4 Code size Grows with each use One copy
5 Type checking None Full
6 Argument evaluation May happen several times Exactly once
7 Debugging Hard — the name disappears Easy — appears in the stack
8 Works for any type Yes Only the declared types
9 Can recurse No Yes

Predefined macros:

Macro Expands to
__FILE__ Current source file name as a string
__LINE__ Current line number as an integer
__DATE__ Compilation date, e.g. "Aug 28 2026"
__TIME__ Compilation time, e.g. "16:20:31"
__STDC__ 1 if the compiler conforms to standard C
__func__ Current function name (a predefined identifier, not a macro)
Topic 4.3

File inclusion

#include copies the entire contents of a file into the current source at that point.


#include <stdio.h>     /* system header  — search system paths  */
#include "myutils.h"   /* your header    — search here first    */
Form Searched first Then Use for
#include <file> Standard / system include directories Paths given with -I Library headers
#include "file" Directory of the current file Then the system paths Your own headers

Header guards — stop a header being pasted twice:


/* myutils.h */
#ifndef MYUTILS_H
#define MYUTILS_H

int add(int a, int b);
int sub(int a, int b);

#endif /* MYUTILS_H */

The first inclusion defines MYUTILS_H; the second finds it already defined and skips everything to #endif.

Circular inclusion:


/* a.h includes b.h, and b.h includes a.h → infinite loop */
/* Fixes:                                                 */
/*   1. Put an include guard in EVERY header              */
/*   2. Use forward declarations instead of #include      */
/*   3. Include headers in the .c file, not the .h file   */
Topic 4.4

Conditional compilation


#define LEVEL 2

#if LEVEL == 1
    puts("basic");
#elif LEVEL == 2
    puts("standard");
#else
    puts("advanced");
#endif

#ifdef DEBUG                /* is DEBUG defined?     */
    printf("debug build\n");
#endif

#ifndef MAX                /* is MAX NOT defined?   */
    #define MAX 100          /* provide a default     */
#endif

#if defined(LINUX) || defined(MAC)
    puts("unix-like");
#endif
# Directive Meaning
1 #if expr Include the block if the constant expression is non-zero
2 #elif expr Else-if for the preprocessor
3 #else Otherwise
4 #endif Ends every conditional block
5 #ifdef NAME True if NAME is defined (same as #if defined(NAME))
6 #ifndef NAME True if NAME is not defined
7 defined(NAME) Operator usable inside #if and #elif

Where it is used:

  • Debug builds — tracing that disappears in release
  • Cross-platform code — different code per operating system
  • Feature toggles — ship one code base with optional features
  • Include guards — the #ifndef pattern
  • Commenting out code — #if 0 … #endif nests safely, unlike /* */
#ifdef tests existence, not value. With #define FLAG 0, the #ifdef FLAG branch is still taken. Use #if FLAG when the value matters.
Topic 4.5

Error, warning, line, and pragma directives

#error — stop the build deliberately:


#ifndef CONFIG_H
    #error "config.h must be included first"
#endif

#if __STDC_VERSION__ < 199901L
    #error "This code requires C99 or later"
#endif

#warning — a message without failing (a common extension):


#ifdef USE_OLD_API
    #warning "USE_OLD_API is deprecated"
#endif

#line — override the reported line number and file name:


#line 100                    /* next line is treated as line 100 */
#line 50 "generated.c"     /* also change __FILE__            */

printf("%s:%d\n", __FILE__, __LINE__);

Its real use is in code generators such as lex and yacc, so that error messages point at the original input file rather than the generated C.

#pragma — compiler-specific instructions:

Pragma Effect Support
#pragma once Include this file at most once All major compilers (not standard)
#pragma pack(1) Remove structure padding MSVC, GCC, Clang
#pragma warning(disable: 4996) Silence a specific warning MSVC
#pragma GCC diagnostic ignored "..." Silence a specific warning GCC, Clang
#pragma message("...") Print a build-time message MSVC, GCC
An unrecognised #pragma is ignored rather than an error, which is what makes it safe for compiler-specific hints — but also easy to misspell silently.
Topic 4.6

Preprocessor operators — #, ##, and defined()

Stringizing with #:


#define STR(x)  #x

printf("%s\n", STR(hello));      /* prints hello */

/* Print a variable's NAME and value */
#define SHOW(v)  printf(#v " = %d\n", (v))

int count = 42;
SHOW(count);                    /* count = 42 */

Token pasting with ##:


#define CAT(a, b)  a##b

int var1 = 10, var2 = 20;
printf("%d\n", CAT(var, 1));   /* becomes var1 → 10 */

/* Generating similar function names */
#define MAKE_GETTER(type, name)      \
    type get_##name(void) { return name; }

The two-level stringize trick:


#define STR(x)   #x
#define XSTR(x)  STR(x)

#define VERSION 3

STR(VERSION)    /* "VERSION" — the name  */
XSTR(VERSION)   /* "3"       — the value */
Operator Name Does Example
# Stringize Turns an argument into a string literal #x → "x"
## Token paste Joins two tokens into one a##b → ab
defined() Defined test 1 if the name is defined, else 0 #if defined(X)
\ Line continuation Extends a directive to the next line Multi-line macros
Topic 4.7

Variadic macros, include guards, and platform detection

Variadic macros — ... and __VA_ARGS__:


#define LOG(fmt, ...)  \
    fprintf(stderr, "[%s:%d] " fmt "\n", __FILE__, __LINE__, __VA_ARGS__)

LOG("value = %d", x);
/* [main.c:42] value = 10 */

/* Handling an EMPTY argument list */
/* GNU extension:   ##__VA_ARGS__  removes the stray comma */
#define LOG2(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)

/* C23 standard way: __VA_OPT__(,) */
#define LOG3(fmt, ...) fprintf(stderr, fmt __VA_OPT__(,) __VA_ARGS__)

Include guards — two styles:

Aspect #ifndef guard #pragma once
Standard C Yes No — an extension
Portability Every compiler All major compilers
Lines needed Three One
Name collisions Possible if the guard name repeats Impossible
Same file via two paths Handled correctly May be included twice on some setups
Speed Slightly slower Slightly faster

Debug builds:


#ifdef DEBUG
    #define TRACE(...) fprintf(stderr, __VA_ARGS__)
#else
    #define TRACE(...) /* nothing at all */
#endif

TRACE("x = %d\n", x);   /* compile with -DDEBUG to enable */

Platform and compiler detection:


#if defined(_WIN32)
    #define PLATFORM "Windows"
#elif defined(__linux__)
    #define PLATFORM "Linux"
#elif defined(__APPLE__)
    #define PLATFORM "macOS"
#else
    #define PLATFORM "Unknown"
#endif

#if defined(__GNUC__)
    #define COMPILER "GCC"
#elif defined(_MSC_VER)
    #define COMPILER "MSVC"
#elif defined(__clang__)
    #define COMPILER "Clang"
#endif
Example

Sample program — enum, typedef, and the preprocessor together


/* One program touching every idea in this level */
#include <stdio.h>

#define APP_NAME     "Demo"
#define MAX_ITEMS    5
#define SQUARE(x)    ((x) * (x))
#define SHOW(v)       printf(#v " = %d\n", (v))
#define LOG(fmt, ...) \
    printf("[%s:%d] " fmt "\n", __FILE__, __LINE__, __VA_ARGS__)

typedef enum { LOW, MEDIUM, HIGH } Priority;

typedef struct {
    char     title[30];
    Priority level;
} Task;

const char *priorityName[] = { "LOW", "MEDIUM", "HIGH" };

int main(void) {
    Task t = { "Write report", HIGH };
    int n = 4;

    printf("%s — max %d items\n", APP_NAME, MAX_ITEMS);

    printf("task: %s, priority %s (%d)\n",
           t.title, priorityName[t.level], t.level);

    SHOW(n);                       /* n = 4  — # operator */
    printf("square = %d\n", SQUARE(2 + 3));   /* 25 */

    LOG("finished with n = %d", n);

#ifdef DEBUG
    printf("debug build, compiled %s %s\n", __DATE__, __TIME__);
#endif

    return 0;
}
What to observe: the enum gives the priority a readable name, the typedef hides struct, the parenthesized macro survives 2 + 3, #v prints the variable's own name, and the #ifdef block only exists when you compile with -DDEBUG.

Level 2 Advanced Content

Going deeper

Level 1 covered enum, typedef, and every directive. Level 2 explains the expansion algorithm, why macros are not recursive, multiple evaluation of arguments, assertions and debug builds, X-macros, header design, and common mistakes.

Topic 1

How expansion actually works

  • The preprocessor rescans the replacement text and expands macros found there
  • Arguments are expanded before substitution, unless used with # or ##
  • That exception is exactly why the two-level XSTR trick is needed
  • Expansion stops when no macro names remain

#define A  B
#define B  10

A   /* A → B → 10 : rescanning keeps going */

/* See the result yourself */
/*   gcc -E  file.c            full expansion   */
/*   gcc -dM -E file.c         every macro       */
Topic 2

Why macros are not recursive

While a macro is being expanded, its own name is marked as unavailable — often called "blue painting". The inner occurrence is left as ordinary text.


#define A  A + 1

A    /* expands ONCE to  A + 1  and stops */

#define F(x)  F(x) + 1

F(2)  /* becomes F(2) + 1 — the inner F is untouched */
  • This is a deliberate rule that guarantees preprocessing terminates
  • Bounded "recursion" is possible by defining several numbered macros, but it is unreadable
  • If you need real recursion, write a function — that is what they are for
Topic 3

The multiple evaluation trap


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

int i = 5, j = 3;
int m = MAX(i++, j);

/* Expands to:  ((i++) > (j)) ? (i++) : (j)   */
/* i is incremented TWICE — a real bug        */
  • Never pass an expression with side effects to a macro
  • Prefer an inline function — arguments are evaluated exactly once
  • Watch out for i++, ++i, and function calls in macro arguments

/* Safe alternative */
static inline int maxInt(int a, int b) {
    return (a > b) ? a : b;
}
Topic 4

Debugging, assertions, and NDEBUG


#include <assert.h>

assert(n > 0);   /* aborts with file and line if false */

/* Compiling with -DNDEBUG removes every assert entirely */

/* Levelled tracing, all removed in release builds */
#ifdef DEBUG
    #define DBG(...)   do {                                     \
        fprintf(stderr, "%s:%d:%s: ",                        \
                __FILE__, __LINE__, __func__);                \
        fprintf(stderr, __VA_ARGS__);                        \
    } while (0)
#else
    #define DBG(...)   ((void)0)
#endif
Conditional compilation beats an if (debug) check: the disabled code never reaches the compiler, so it costs no space and no runtime test.
Topic 5

X-macros — one list, many uses

An X-macro keeps an enum and its name table in sync automatically, removing the classic bug where you add an enum member but forget the string.


/* The single source of truth */
#define COLOR_LIST  \
    X(RED)           \
    X(GREEN)         \
    X(BLUE)

/* Use 1 — build the enum */
#define X(name)  name,
enum Color { COLOR_LIST COLOR_COUNT };
#undef X

/* Use 2 — build the matching name table */
#define X(name)  #name,
const char *colorName[] = { COLOR_LIST };
#undef X

printf("%s\n", colorName[GREEN]);   /* GREEN */
Topic 6

Designing a header file


/* mathutils.h */
#ifndef MATHUTILS_H
#define MATHUTILS_H

#include <stddef.h>      /* only what the header itself needs */

#define MU_PI 3.14159265358979

typedef struct { double x, y; } MuPoint;

double mu_distance(MuPoint a, MuPoint b);
double mu_area(double r);

#endif /* MATHUTILS_H */
  • Guard everything — a guard in every header, named after the path
  • Declarations only — definitions belong in the .c file
  • Prefix public names — mu_ here, to avoid collisions
  • Include minimally — every extra #include spreads to all users
  • Never put static variable definitions in a header
Topic 7

Common mistakes

Mistake Why it hurts Fix
#define MAX 100; The semicolon is substituted too No semicolon on a #define
#define SQ(x) x*x SQ(2+3) gives 11 ((x)*(x))
#define AREA(r) 3.14*r*r used as 1/AREA(2) Whole body unparenthesized Wrap the entire body too
Space before ( in a macro definition Becomes an object-like macro #define F(x), never #define F (x)
MAX(i++, j) i incremented twice Use an inline function
Header with no include guard Redefinition errors Add #ifndef / #define / #endif
#ifdef when you meant the value Takes the branch even for 0 Use #if
Missing #endif Confusing errors far from the cause Comment each #endif with its condition
Multi-statement macro without do while (0) Breaks inside a braceless if Use the do while (0) wrapper
#define P char * then P a, b; b is not a pointer Use typedef char *P;
Expecting a macro in the debugger It no longer exists Use const or enum
Enum value assumed unique or range-checked Neither is enforced Validate before use

Level 3 C Tricky Interview Points

Advanced tricky interview points on enum, typedef, macro expansion, conditional compilation, stringizing, token pasting, and predefined macros.

Tricky Interview Point-1:

The preprocessor runs before the compiler and knows no C at all

It performs pure text substitution on the source. It has no idea what a type, a variable, or a scope is, which is the root cause of almost every macro surprise.

Tricky Interview Point-2:

A macro has no type and no scope — a const or enum does

#define MAX 100 disappears before the compiler sees it, so the debugger cannot show it. enum { MAX = 100 }; and const int give you a real, typed, scoped symbol.

Tricky Interview Point-3:

Missing parentheses in a macro is the classic bug

#define SQ(x) x*x makes SQ(2+3) expand to 2+3*2+3 = 11. Wrap both the whole body and every parameter: #define SQ(x) ((x)*(x)).

Tricky Interview Point-4:

Macro arguments with side effects are evaluated more than once

MAX(a++, b) expands to a comparison that increments a twice. An inline function evaluates each argument exactly once, which is why functions are preferred when possible.

Tricky Interview Point-5:

Never put a semicolon after a #define

#define MAX 100; makes int a[MAX]; expand to int a[100;];. The preprocessor copies the semicolon along with the value.

Tricky Interview Point-6:

#include <file> searches system directories, "file" searches locally first

Angle brackets are for standard and system headers; quotes start in the directory of the including file and then fall back to the system path.

Tricky Interview Point-7:

#include literally pastes the entire file into your source

That is why a header included twice causes redefinition errors, and why include guards exist. The compiler sees one large translation unit, not several files.

Tricky Interview Point-8:

Include guards prevent double inclusion; #pragma once is not standard

#ifndef HDR_H / #define HDR_H / ... / #endif works everywhere. #pragma once is shorter and supported by every major compiler, but it is an extension, not standard C.

Tricky Interview Point-9:

#ifdef checks whether a name is defined, not whether it is true

#define FLAG 0 followed by #ifdef FLAG still takes the branch. Use #if FLAG when you care about the value and #ifdef when you care about existence.

Tricky Interview Point-10:

Conditional compilation removes code entirely — it is not an if statement

Code inside a false #if never reaches the compiler, so it is not even syntax checked, and it costs nothing at runtime. An if statement compiles both branches.

Tricky Interview Point-11:

The # operator turns a macro argument into a string literal

#define STR(x) #x makes STR(hello) become "hello". Two levels are needed to stringize a macro's value rather than its name: #define XSTR(x) STR(x).

Tricky Interview Point-12:

## pastes two tokens into one new token

#define CAT(a,b) a##b makes CAT(var, 1) become var1. The result must be a valid single token, otherwise the behaviour is undefined.

Tricky Interview Point-13:

Macros are not recursive — a macro never expands inside itself

If #define A A+1 is expanded, the inner A is marked "blue-painted" and left alone, so you get A+1 rather than an infinite loop.

Tricky Interview Point-14:

__LINE__ and __FILE__ expand at the point of use, which makes them ideal for logging

They report where the macro was written, not where it was defined, so a LOG macro automatically records the caller's location. __func__ is not a macro — it is a predefined identifier.

Tricky Interview Point-15:

__VA_ARGS__ carries a variable number of macro arguments

#define LOG(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__) forwards everything. The GNU ##__VA_ARGS__ trick (standardised as __VA_OPT__ in C23) removes the comma when the list is empty.

Tricky Interview Point-16:

Enum constants are int and start at 0 unless you assign values

enum Color { RED, GREEN, BLUE }; gives 0, 1, 2. Assigning one value makes the following members continue from it: { A = 5, B, C } yields 5, 6, 7.

Tricky Interview Point-17:

Enum values need not be unique, and there is no automatic range checking

Two members can share a value, and assigning 99 to an enum variable compiles fine. The enum documents intent; it does not enforce it.

Tricky Interview Point-18:

typedef creates an alias, not a new type

typedef int Length; means Length and int are interchangeable — the compiler will not warn if you mix them. It improves readability, not type safety.

Tricky Interview Point-19:

typedef is the readable way to declare function pointers

typedef int (*Compare)(const void *, const void *); turns an unreadable declaration into a name you can use as a parameter or return type.

Tricky Interview Point-20:

typedef obeys scope; #define does not

A typedef inside a function is local to it and respects blocks. A #define is active from its line until #undef or the end of the file, regardless of braces.

Interview tip: the single most reliable answer is "the preprocessor does text substitution and knows nothing about C". Almost every macro question follows from it.

Level 4 Simple Programs

Short programs covering enum, typedef, macros, conditional compilation, operators, and predefined macros.

Program 1 — Enum for days of the week


/* Names and their integer values */
#include <stdio.h>

enum Day { MON, TUE, WED, THU, FRI, SAT, SUN, DAY_COUNT };

int main(void) {
    const char *name[] = {
        "Monday", "Tuesday", "Wednesday", "Thursday",
        "Friday", "Saturday", "Sunday"
    };
    int d;

    for (d = MON; d < DAY_COUNT; d++)
        printf("%d = %s\n", d, name[d]);

    return 0;
}

Program 2 — Enum with explicit values in a switch


/* Status codes with chosen numbers */
#include <stdio.h>

typedef enum {
    STATUS_OK       = 200,
    STATUS_NOTFOUND = 404,
    STATUS_ERROR    = 500
} Status;

int main(void) {
    Status s = STATUS_NOTFOUND;

    switch (s) {
        case STATUS_OK:
            printf("%d OK\n", s);
            break;
        case STATUS_NOTFOUND:
            printf("%d Not Found\n", s);
            break;
        case STATUS_ERROR:
            printf("%d Server Error\n", s);
            break;
    }

    return 0;
}

Program 3 — typedef for a structure and a function pointer


/* Two functions called through one alias */
#include <stdio.h>

typedef struct {
    char name[30];
    int  roll;
} Student;

typedef int (*Op)(int, int);

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }

int main(void) {
    Student s = { "Asha", 101 };
    Op op;

    printf("%s %d\n", s.name, s.roll);

    op = add;
    printf("add = %d\n", op(4, 5));

    op = mul;
    printf("mul = %d\n", op(4, 5));

    return 0;
}

Program 4 — Macros with parameters


/* Parentheses make these safe for expressions */
#include <stdio.h>

#define SQUARE(x)     ((x) * (x))
#define MAXV(a, b)    (((a) > (b)) ? (a) : (b))
#define MINV(a, b)    (((a) < (b)) ? (a) : (b))
#define IS_EVEN(n)    ((n) % 2 == 0)

int main(void) {
    int a = 7, b = 12;

    printf("SQUARE(2+3) = %d\n", SQUARE(2 + 3));
    printf("MAXV        = %d\n", MAXV(a, b));
    printf("MINV        = %d\n", MINV(a, b));
    printf("%d is %s\n", a, IS_EVEN(a) ? "even" : "odd");

    return 0;
}

Program 5 — Predefined macros and conditional compilation


/* Compile with -DDEBUG to see the extra output */
#include <stdio.h>

int main(void) {
    printf("file : %s\n", __FILE__);
    printf("line : %d\n", __LINE__);
    printf("date : %s\n", __DATE__);
    printf("time : %s\n", __TIME__);
    printf("func : %s\n", __func__);

#ifdef __STDC__
    printf("standard C compiler\n");
#endif

#if defined(_WIN32)
    printf("platform: Windows\n");
#elif defined(__linux__)
    printf("platform: Linux\n");
#elif defined(__APPLE__)
    printf("platform: macOS\n");
#endif

#ifdef DEBUG
    printf("[debug build]\n");
#endif

    return 0;
}

Program 6 — Stringizing, token pasting, and a variadic log macro


/* The # and ## operators plus __VA_ARGS__ */
#include <stdio.h>

#define SHOW(v)       printf(#v " = %d\n", (v))
#define CAT(a, b)     a##b
#define LOG(fmt, ...) \
    printf("[%s:%d] " fmt "\n", __FILE__, __LINE__, __VA_ARGS__)

int main(void) {
    int count = 42;
    int var1 = 10, var2 = 20;

    SHOW(count);                       /* count = 42 */

    printf("var1 = %d\n", CAT(var, 1));
    printf("var2 = %d\n", CAT(var, 2));

    LOG("sum = %d", var1 + var2);

    return 0;
}
Comment Directive Type Keyword Function Variable String Number

Level 5 Problem Solving Tricks

How to master enum, typedef, and the preprocessor

Remember one sentence — the preprocessor performs text substitution and understands no C — and then adopt a few defensive habits. Almost every macro bug is a missing parenthesis, a stray semicolon, or an argument evaluated twice.

Trick 1

Parenthesize every macro parameter and the whole body

#define SQ(x) ((x)*(x)) is correct; #define SQ(x) x*x is a bug waiting for the first caller who passes an expression.

Trick 2

Prefer const, enum, or inline over a macro

A macro is the last resort. const and enum give you types and a debugger view; an inline function gives you single evaluation of its arguments.

Trick 3

Wrap multi-statement macros in do { ... } while (0)

That makes the macro behave like a single statement, so it works correctly as the body of an if without braces and still accepts a trailing semicolon.

Trick 4

Put an include guard in every header, without exception

Use a name derived from the path, like PROJECT_UTILS_H, so two headers with the same file name in different folders cannot collide.

Trick 5

Use #ifdef DEBUG for tracing you can switch off

Compile with -DDEBUG while developing and leave it out in release. The debug code vanishes entirely instead of costing a runtime check.

Trick 6

Build log macros from __FILE__ and __LINE__

#define LOG(...) fprintf(stderr, "%s:%d: ", __FILE__, __LINE__), fprintf(stderr, __VA_ARGS__) gives you free location tracking on every message.

Trick 7

Inspect the expansion when a macro misbehaves

gcc -E file.c stops after preprocessing and shows the real text the compiler will see. Most macro mysteries are solved in one command.

Trick 8

Use enum for related constants instead of several #defines

enum { MON, TUE, WED } numbers them automatically, keeps them grouped, and lets the debugger print the names.

Quick success checklist

  • Before run: no semicolon after #define? every parameter parenthesized? every #if has an #endif?
  • Debug order: run gcc -E → read the real expansion → substitute the arguments by hand
  • Design habit: prefer const/enum/inline → guard every header → keep debug code behind #ifdef DEBUG
Level-5 goal: use the preprocessor for what only it can do — inclusion, configuration, and conditional compilation — and let the language handle constants, types, and functions.

Level 6 Practice Programs List

Programming questions only — focused on enum, typedef, macros, conditional compilation, include guards, preprocessor operators, variadic macros, and predefined macros.

  1. 1. Write a C program that defines an enum for the days of the week and prints each day name with its integer value.
  2. 2. Write a C program that uses an enum with explicitly assigned values (for example status codes) inside a switch statement.
  3. 3. Write a C program that uses typedef to create an alias for a structure and declares variables with the short name.
  4. 4. Write a C program that uses typedef for a function pointer and calls two different functions through the same alias.
  5. 5. Write a C program that defines a symbolic constant with #define and uses it as an array size.
  6. 6. Write a C program with a function-like macro SQUARE(x) that works correctly for SQUARE(2+3).
  7. 7. Write a C program with macros MAX(a,b) and MIN(a,b) and test them with expressions.
  8. 8. Write a C program that prints __FILE__, __LINE__, __DATE__, __TIME__, and __func__.
  9. 9. Write a C program that uses #ifdef DEBUG so that trace messages appear only when DEBUG is defined.
  10. 10. Write a C program that uses #if, #elif, #else, and #endif to select one of three code blocks based on a macro value.
  11. 11. Write a C program with a header file protected by an include guard, and include it twice from the same source file.
  12. 12. Write a C program that uses the # operator to print the name of a variable along with its value.
  13. 13. Write a C program that uses the ## operator to build variable names such as var1 and var2.
  14. 14. Write a C program with a variadic LOG macro using __VA_ARGS__ that prefixes every message with the file and line number.
  15. 15. Write a C program that detects the operating system using _WIN32, __linux__, or __APPLE__ and prints the platform name.