The preprocessor
The preprocessor runs before the compiler and works on text: it pastes included files, replaces macros and removes the lines of a false conditional. It knows nothing about types or scopes, which is why its mistakes look strange in the compiler's messages. The output of this stage is what lesson 1 called the first of the four build stages.
Includes and guards
#include pastes one file into another. Quotes search the directory of the including file first, angle brackets the system paths. The guard around the declarations is the one from lesson 19, and the preprocessor replaces both the constants before the compiler sees the code.
vi config.h
#ifndef CONFIG_H
#define CONFIG_H
#define APP_NAME "preproc"
#define BUFFER_SIZE 8
#endif
vi main.c
#include <stdio.h>
#include "config.h"
int main(void)
{
char buffer[BUFFER_SIZE] = {0};
printf("%s %zu\n", APP_NAME, sizeof buffer);
return 0;
}
gcc -Wall -Wextra main.c -o app
./app
preproc 8
The compiler sees char buffer[8] and printf("%s %zu\n", "preproc", sizeof buffer); the names are gone. gcc -E main.c prints exactly that expanded text, which is the fastest way to see what a macro really produced.
Object-like macros
An object-like macro is a name replaced by a token sequence. It has no type, so the compiler checks the use only after replacement. An existing macro can be removed with #undef and defined again with another value, and the change applies from that line onward.
#include <stdio.h>
#define LIMIT 5
#define NAME "sample"
int main(void)
{
printf("%s limit %d\n", NAME, LIMIT);
#undef LIMIT
#define LIMIT 9
printf("new limit %d\n", LIMIT);
return 0;
}
gcc -Wall -Wextra object.c -o object
./object
sample limit 5
new limit 9
For a value with a type, const from lesson 39 is usually better: the debugger sees it and the compiler checks it. Macros remain the right tool for array sizes and for text that must not be evaluated.
Predefined macros
The compiler defines a set of macros itself. __FILE__ and __LINE__ expand at the point of use, which makes them useful in error messages; __func__ is the name of the enclosing function; __STDC_VERSION__ is the language level.
#include <stdio.h>
static void report(void)
{
printf("file: %s\n", __FILE__);
printf("line: %d\n", __LINE__);
printf("function: %s\n", __func__);
printf("standard: %ld\n", __STDC_VERSION__);
}
int main(void)
{
report();
return 0;
}
gcc -Wall -Wextra predefined.c -o predefined
./predefined
file: predefined.c
line: 6
function: report
standard: 202311
The line is six because that is the line of the printf call after expansion, not the line of the definition. The file name is the one passed to the compiler, so building from another directory changes it to a path.
Conditional compilation
#ifdef keeps a block only when a macro is defined, and #if 0 removes one unconditionally. The removed text is not compiled and not syntax-checked, which is why it is the safe way to disable a block that contains comments or invalid code. The same source builds two programs here.
#include <stdio.h>
int main(void)
{
#ifdef DEBUG
printf("debug build\n");
#else
printf("release build\n");
#endif
#if 0
printf("this line is removed\n");
#endif
printf("end\n");
return 0;
}
gcc -Wall -Wextra conditional.c -o conditional_release
./conditional_release
gcc -DDEBUG -Wall -Wextra conditional.c -o conditional_debug
./conditional_debug
release build
end
debug build
end
-DDEBUG defines the macro on the command line, so the build decides which branch exists. The removed block never reaches the compiler, and the two binaries differ only in that one message.
Undefined names and stray directives
An undefined name in #if silently evaluates to zero, which is usually not what the author meant. With -Wundef the compiler reports the name and the line:
#include <stdio.h>
int main(void)
{
#if FEATURE
printf("feature on\n");
#else
printf("feature off\n");
#endif
return 0;
}
gcc -Wall -Wextra -Wundef undef.c -o undef
./undef
undef.c: In function 'main':
undef.c:5:5: warning: 'FEATURE' is not defined, evaluates to '0' [-Wundef]
5 | #if FEATURE
| ^~~~~~~
feature off
The second mistake is a directive without its opening pair. The preprocessor counts #if and #endif and rejects the file before the compiler sees it:
#include <stdio.h>
int main(void)
{
printf("start\n");
#endif
return 0;
}
gcc -Wall -Wextra stray.c -o stray
stray.c: In function 'main':
stray.c:6:2: error: '#endif' without '#if'
6 | #endif
| ^~~~~
The message names the directive, not a function, even though the line sits inside main. Errors from this stage almost always point at the directive itself; read the line above it to find the missing partner.
Exercises
- Add a VERSION macro to config.h and print it with a string next to it.
- Build the conditional program with -DDEBUG=1 and with -DDEBUG=0. Does the second define take the debug branch?
- Write an assertion macro that prints __FILE__, __LINE__ and __func__ when its condition is false.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does the preprocessor know about the C program it processes?
Answer
Only text, not types or scopes
It pastes includes and replaces macros as text, so the compiler is the first stage that understands types.
-
How does
#include "config.h"find the header?Answer
The including file directory comes first
Quoted includes search beside the source first and then the system paths, while angle brackets use the system paths only.
-
Why prefer
constover an object-like macro for a value?Answer
It has a type and the debugger can see it
A
constobject is checked and visible to the debugger; a macro is only text and has no type. -
What does
#if 0do to the block that follows?Answer
It removes the text before compilation
The preprocessor drops the block between
#if 0and#endif, so it is never compiled or checked. -
What does this program print?
#include <stdio.h> #define APP_NAME "preproc" #define BUFFER_SIZE 8 int main(void){ char buffer[BUFFER_SIZE] = {0}; printf("%s %zu\n", APP_NAME, sizeof buffer); return 0; }Answer
preproc 8
APP_NAMEexpands to the stringpreproc, andsizeof bufferis the eight bytes of the array. -
What does this program print?
#include <stdio.h> #define LIMIT 5 int main(void) { printf("limit %d\n", LIMIT); #undef LIMIT #define LIMIT 9 printf("new limit %d\n", LIMIT); return 0; }Answer
limit 5 / new limit 9
The first print sees
LIMITas 5;#undefand a new definition make the second 9. -
What does this program print?
#include <stdio.h> static void report(void){ printf("function: %s\n", __func__); } int main(void){ report(); return 0; }Answer
function: report
__func__is the name of the enclosing function, which isreport. -
What does this program print?
#include <stdio.h> int main(void){ #ifdef DEBUG printf("debug build\n"); #else printf("release build\n"); #endif return 0; }Answer
release build
Without
-DDEBUGthe#elsebranch is kept, so the release message prints. -
What happens with
#if FEATUREwhenFEATUREis undefined?Answer
It evaluates to zero and warns with
-WundefAn undefined name in
#ifbecomes zero, and-Wundefreports the name and its line. -
What does GCC report for a stray
#endif?Answer
#endif without #ifat the directiveThe preprocessor counts the conditional directives and rejects the unmatched
#endifbefore compilation.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev