Menu

The C Preprocessor: #include, #define, and What Runs Before Compilation

The preprocessor edits your source text before the compiler ever sees it - pasting in headers, substituting macros, and switching code in or out. Here is the whole directive family and how to look at its output.

This page includes runnable editors - edit, run, and see output instantly.

Every C file you have ever compiled started with a line like #include <stdio.h>, and that line is not C. It is an instruction to a separate program - the preprocessor - that runs first, rewrites your source text, and hands the result to the compiler.

Understanding that two-stage split explains a lot of C's behavior: why headers are pasted rather than imported, why a bad macro produces an error on a line that looks perfectly fine, and why the same source file can compile to different programs on different systems.

What Actually Happens Before Compilation

Compiling a .c file is not one step. Roughly, it is four:

  1. Preprocessing - obey every line starting with #, producing one big expanded source text (a translation unit).
  2. Compiling - turn that text into assembly, then into an object file.
  3. Assembling - produce machine code.
  4. Linking - join the object files with the libraries into an executable.

The preprocessor works purely on text. It does not know what a variable is, what a type is, or whether your braces balance. It sees characters and tokens, replaces some of them with others, and moves on. Everything strange about macros follows from that one fact.

By the time the compiler reads this program, there is no GREETING anywhere. The preprocessor has already replaced it with the literal "Hello from a macro", exactly as if you had typed it.

The Directive Family

Every preprocessor directive begins with # as the first non-whitespace character on its line. There is no semicolon, and the directive ends at the end of the line unless you continue it with a backslash.

DirectiveWhat it does
#includePaste in the contents of another file
#defineDefine a macro (a text substitution)
#undefRemove a macro definition
#ifdef, #ifndefKeep the following code only if a macro is (not) defined
#if, #elif, #else, #endifKeep code based on a constant expression
#errorStop compilation with a message
#pragmaCompiler-specific instruction, such as #pragma once
#lineChange the reported line number (rare)

Three of these get pages of their own: macros covers #define in depth, conditional compilation covers the #if family, and header files covers how #include is used to structure a multi-file program.

#include: Angle Brackets vs Quotes

#include does exactly one thing: it replaces its own line with the entire contents of the named file. That included file is itself preprocessed, so its own #include lines expand too.

#include <stdio.h>     /* search the system include directories */
#include "config.h"    /* search this file's directory first */

The difference is the search order:

  • <angle brackets> look in the compiler's standard include directories - /usr/include, the toolchain's own folders, plus anything you add with -I. This is for library headers.
  • "quotes" look first in the directory containing the file doing the including, then fall back to the same list as angle brackets. This is for headers you wrote.

Both forms work for either kind of header on most compilers, but the convention carries meaning: angle brackets say "this is someone else's header," quotes say "this is mine." Mixing them up is how a project ends up including a stale system header instead of its own.

Because inclusion is textual pasting, including the same header twice pastes it twice - which is why headers need include guards. That is the first thing the header files page fixes.

#define: Text Substitution, Nothing More

#define NAME replacement tells the preprocessor: from here on, wherever the token NAME appears, put replacement instead.

Note what is not happening here. MAX_USERS has no type. It is not stored anywhere. It cannot be inspected in a debugger. It is a find-and-replace rule, and after preprocessing the program literally contains printf("%s allows %d users\n", "Coddy", 100);.

That also means the substitution is blind. This compiles, and does something surprising:

#define SIZE 5 + 1

int arr[SIZE];          /* fine: int arr[5 + 1]; */
int total = SIZE * 2;   /* 5 + 1 * 2 == 7, not 12 */

The fix - parentheses around everything - is the central rule of the macros page, along with macros that take arguments.

A #define with no replacement text defines the name as "present but empty". That is useless as a substitution and essential as a flag:

#define DEBUG          /* defined, expands to nothing */

Code can then ask whether DEBUG exists with #ifdef.

Seeing the Preprocessor's Output

The best way to build intuition is to look at what the preprocessor actually produced. gcc -E stops after preprocessing and prints the result:

gcc -E hello.c

For a file that includes <stdio.h>, that is 700 to 30,000 lines depending on your system - almost all of it the header's own contents. To see just your part, take the tail:

gcc -E hello.c | tail -20

Try it on a file like this one:

#define SQUARE(x) ((x) * (x))
#define LIMIT 10

int main(void) {
    int n = SQUARE(LIMIT);
    return n;
}

The tail of the output shows:

int main(void) {
    int n = ((10) * (10));
    return n;
}

Every macro is gone; only substituted text remains. When a macro misbehaves, this command tells you why in seconds, and it beats guessing. Two useful companions: gcc -dM -E - < /dev/null lists every macro your compiler predefines, and gcc -E -P file.c omits the line-marker noise.

Why Errors Point at the Wrong Line

Because the compiler sees expanded text, a mistake inside a macro is reported where the macro was used, not where it was written:

#define HALF(x) (x / 2

int main(void) {
    int y = HALF(8);   /* error reported here */
    return 0;
}

The missing parenthesis is in the #define line, but the compiler complains about the line containing HALF(8), often with a message about an unexpected token that makes no sense in context. When an error looks impossible, expand the file with gcc -E and read the real line.

Modern compilers help: GCC and clang print an "in expansion of macro" note pointing back at the definition. Compile with -Wall -Wextra so you actually see those notes.

Predefined Macros

The preprocessor supplies some macros itself. These are genuinely useful for diagnostics:

__FILE__ and __LINE__ expand to the current file name and line number, which is how assertion and logging macros report where something went wrong. (__func__ is slightly different - it is a real identifier the compiler provides, not a preprocessor macro, but it is used the same way.)

Compilers also predefine platform macros such as __linux__, _WIN32, and __APPLE__. Code that must differ per system tests those with #ifdef, which is the subject of conditional compilation.

What to Take Away

The preprocessor is small and it is dumb, and both are on purpose. It gives you three powers - pull in a file, substitute text, switch code on and off - and no type checking whatsoever to go with them.

That trade is why the standard advice is to reach for language features first: use const int or an enum instead of #define for constants when you can, and a real function instead of a function-like macro. Where the preprocessor is genuinely the right tool - headers, portability switches, compile-time configuration - it is irreplaceable.

Next, macros takes #define seriously: arguments, the parentheses rules, and the traps that come with substituting text into code you did not write.

Frequently Asked Questions

What is the preprocessor in C?

A text-processing step that runs before compilation. It obeys the lines beginning with # - pasting the contents of header files in place of #include, substituting macros defined with #define, and deleting or keeping code according to #if/#ifdef. The compiler only ever sees the result, never your original file.

What is the difference between #include <stdio.h> and #include "myfile.h"?

Angle brackets search the compiler's system include directories, which is where the standard library headers live. Quotes search the directory of the current file first, then fall back to the system paths. Use angle brackets for library headers and quotes for headers you wrote.

How can I see what the preprocessor produced?

Run gcc -E file.c to stop after preprocessing and print the expanded source. On a file that includes <stdio.h> the output is thousands of lines, so pipe it: gcc -E file.c | tail -30 shows just your own code with every macro already substituted.

Is #include a C statement?

No. Directives are not part of the C language proper - they have their own line-based syntax, they take no semicolon, and they are gone by the time the compiler parses your program. That is why a mistake in a macro shows up as a confusing error on a line that looks fine.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED