Conditional Compilation
#ifdef, #ifndef, #if, include guards, and using them to compile different code for different situations.
Code That Is Not Always There
Last lesson the preprocessor rewrote your text; this lesson it decides which text exists at all. Between #ifdef NAME and #endif you can place any region of source, and the preprocessor keeps it only if NAME is currently defined as a macro. If it is not, the region is deleted before the compiler ever sees it: not skipped at run time like a false if, but absent from the translation unit entirely, costing zero bytes and zero instructions.
The classic customer is debug logging:
total = 15
DEBUG was never defined, so the debug printf was cut out and the compiled program contains only the final line. Now compile the identical file as gcc -std=c89 -DDEBUG program.c: the -D flag defines DEBUG from the command line, no edit to the source, and the run becomes five debug: added ... lines ending with debug: added 5, total is now 15, followed by the same total = 15. One source file, two different programs, chosen at compile time.
Note what #ifdef tests: whether the macro is defined at all, not its value. #define DEBUG with an empty body is enough. #ifndef NAME is the mirror image, keeping the region when the macro is not defined, and an #else between them covers both outcomes with one condition.
#if, defined(), and #undef
#ifdef can only ask one yes/no question. #if takes a full constant integer expression, evaluated by the preprocessor using macro values — given #define MAX_USERS 500, this keeps the first branch:
#if MAX_USERS > 100
/* code for the large-table version */
#else
/* code for the small-table version */
#endif
Inside an #if, the operator defined(NAME) yields 1 or 0, which is how you combine definedness tests: #if defined(DEBUG) && !defined(QUIET) has no #ifdef equivalent, and #elif chains further cases the way else if does at run time. The expression must be constant — it is evaluated before compilation, so it cannot mention variables or call functions. Finally, #undef NAME removes a macro definition, after which #ifdef NAME is false again; it is rare, but it is the only way to redefine a macro cleanly.
What Double Inclusion Breaks
Last lesson established that #include is textual paste. That mechanism has a failure mode: in a real project, main.c might include geometry.h directly and include draw.h, which itself includes geometry.h. The contents of geometry.h are now pasted twice, and if it defines a struct, the compiler meets that definition twice:
error: redefinition of 'struct point'
8 | struct point {
| ^~~~~
note: originally defined here
3 | struct point {
| ^~~~~
A second declaration like extern int count; from chapter 9 is harmless, but a second definition of a struct or typedef is a hard error, and you cannot fix it by "just including once" — in a project of fifty headers, nobody can hand-track which header pulls in which.
Include Guards
The fix is the most important application of conditional compilation in C. Every header wraps its entire contents in three lines:
#ifndef GEOMETRY_POINT_H
#define GEOMETRY_POINT_H
/* the header's real contents */
#endif
Read it as a story. First paste: GEOMETRY_POINT_H is not defined, so #ifndef keeps the region; the very next line defines it; the contents compile. Second paste: the macro is defined now, so #ifndef deletes everything down to #endif, and the compiler sees the contents exactly once no matter how many include paths lead here. This pattern is called an include guard, and the rule is absolute: every header you ever write gets one.
This platform's sandbox compiles a single file, so we cannot split a real header off — but because #include is paste, we can simulate double inclusion by writing the same guarded block twice inline:
point is (3, 4)
__STDC__ = 1
/app/code/main.c, line 29
Delete the four guard lines and this program reproduces the redefinition error above; with them, the second struct point never reaches the compiler. Name the guard macro after the header's path — GEOMETRY_POINT_H, not a collision-prone POINT_H like our single-file demo — and never start it with an underscore followed by a capital letter or use a double underscore, since those names are reserved for the implementation. You may meet #pragma once doing this job in other code; it is a compiler extension, not part of ANSI C, so this course uses the guard pattern, which is portable to every compiler ever shipped.
Macros You Never Defined
The last two lines of that output came from macros the implementation defines for you. __STDC__ expands to 1 on every conforming ANSI compiler — the standard way for code to ask "am I being compiled as standard C?". __FILE__ expands to the current file name as a string (here, the path where the sandbox stores your code) and __LINE__ to the current line number as an integer — the raw material of real logging, which is why debug macros so often print them. All follow the reserved double-underscore convention: the implementation may use such names precisely because you may not.
Compiling for Different Situations
Put the pieces together and you have C's mechanism for one codebase, many targets. Real projects select code per platform:
#ifdef _WIN32
/* Windows-specific version */
#else
/* POSIX version */
#endif
Compilers predefine macros identifying the target system, and libraries expose feature knobs the same way: define NDEBUG and the standard assert macro compiles to nothing, exactly the DEBUG trick from the top of this lesson, standardized. The preprocessor cannot ask anything about run time — only about macros and constants — but that is the point: these decisions are made once, at build time, and the excluded branches cost nothing forever after.
Key Takeaways
#ifdef/#ifndef/#else/#endifkeep or delete regions of source before compilation; excluded code is absent from the program, not skipped at run time.-DNAMEon the compile line defines a macro without editing the source, so one file can build into different programs (theDEBUGlogging pattern).#ifevaluates a constant integer expression and supportsdefined(NAME),!,&&, and#elif;#undefremoves a definition.- Double inclusion pastes a header twice, and a second struct definition is a hard error; every header therefore wraps its contents in an include guard:
#ifndef GUARD/#define GUARD/ contents /#endif. - Name guards after the header's path (
GEOMETRY_POINT_H), avoid reserved underscore names, and prefer guards over the non-standard#pragma once; the implementation predefines__STDC__(1 on ANSI compilers),__FILE__, and__LINE__, and platform macros like_WIN32let one codebase target many systems.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Conditional Compilation - Quiz
Test your understanding of the lesson.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!