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

  1. Add a VERSION macro to config.h and print it with a string next to it.
  2. Build the conditional program with -DDEBUG=1 and with -DDEBUG=0. Does the second define take the debug branch?
  3. 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.

  1. 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.

  2. 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.

  3. Why prefer const over an object-like macro for a value?

    Answer

    It has a type and the debugger can see it

    A const object is checked and visible to the debugger; a macro is only text and has no type.

  4. What does #if 0 do to the block that follows?

    Answer

    It removes the text before compilation

    The preprocessor drops the block between #if 0 and #endif, so it is never compiled or checked.

  5. 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_NAME expands to the string preproc, and sizeof buffer is the eight bytes of the array.

  6. 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 LIMIT as 5; #undef and a new definition make the second 9.

  7. 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 is report.

  8. 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 -DDEBUG the #else branch is kept, so the release message prints.

  9. What happens with #if FEATURE when FEATURE is undefined?

    Answer

    It evaluates to zero and warns with -Wundef

    An undefined name in #if becomes zero, and -Wundef reports the name and its line.

  10. What does GCC report for a stray #endif?

    Answer

    #endif without #if at the directive

    The preprocessor counts the conditional directives and rejects the unmatched #endif before 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