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.
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 |
Enum use — why and where
- Readable constants —
if (s == ERROR)beatsif (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 |
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
structandenumkeywords - Portability — change one
typedefto switchinttolongeverywhere - 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 |
#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.
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.
| # | 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 |
#, and ends at the newline — not at
a semicolon. Use a backslash at the end of a line to continue a long directive.
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) |
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 */
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
#ifndefpattern - Commenting out code —
#if 0 … #endifnests 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.
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 |
#pragma is ignored rather than an error, which is what makes it
safe for compiler-specific hints — but also easy to misspell silently.
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 |
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
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;
}
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.
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
XSTRtrick 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 */
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
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
inlinefunction — 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;
}
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
if (debug) check: the disabled code never reaches
the compiler, so it costs no space and no runtime test.
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 */
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
.cfile - Prefix public names —
mu_here, to avoid collisions - Include minimally — every extra
#includespreads to all users - Never put
staticvariable definitions in a header
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.
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;
}
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.
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.
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.
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.
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.
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.
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.
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.
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#ifhas 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 6 Practice Programs List
Programming questions only — focused on enum, typedef, macros, conditional compilation, include guards, preprocessor operators, variadic macros, and predefined macros.
- 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. Write a C program that uses an enum with explicitly assigned values (for example status codes) inside a switch statement.
- 3. Write a C program that uses typedef to create an alias for a structure and declares variables with the short name.
- 4. Write a C program that uses typedef for a function pointer and calls two different functions through the same alias.
- 5. Write a C program that defines a symbolic constant with #define and uses it as an array size.
- 6. Write a C program with a function-like macro SQUARE(x) that works correctly for SQUARE(2+3).
- 7. Write a C program with macros MAX(a,b) and MIN(a,b) and test them with expressions.
- 8. Write a C program that prints __FILE__, __LINE__, __DATE__, __TIME__, and __func__.
- 9. Write a C program that uses #ifdef DEBUG so that trace messages appear only when DEBUG is defined.
- 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. Write a C program with a header file protected by an include guard, and include it twice from the same source file.
- 12. Write a C program that uses the # operator to print the name of a variable along with its value.
- 13. Write a C program that uses the ## operator to build variable names such as var1 and var2.
- 14. Write a C program with a variadic LOG macro using __VA_ARGS__ that prefixes every message with the file and line number.
- 15. Write a C program that detects the operating system using _WIN32, __linux__, or __APPLE__ and prints the platform name.