Electronics/Documentation

Macros and Preprocessor Directives

Last updated Dec 20, 2025Electronics

Macros and Preprocessor Directives

Every now and then, it may be necessary to communicate some extra information to the compiler about your code. Perhaps you are wanting to be able to use some external code, or maybe you want to test for certain conditions to determine what sections need to be included, or maybe you want to be able to control what kinds of errors and warnings your compiler messages about.

These kinds of things are available through what are called “preprocessor directives”. These are one of the first things evaluated during the compilation process, and they are simple modifications directly to the text of your code.

All of these directives start with a #, and indicate some kind of extra information to the compiler

Include

The most common and recognizable of the directives is #include. You will see this in even the most simple of hello world programs, since this is how you can import in parts of the standard library such as printf (or std::cout in C++).

The #include directive takes a single argument: the name or path to a file, wrapped in either double quotes or angle brackets. This file is typically a header file, but it can technically be any file on your computer. In order for the compiler to find the file you are referencing, it must be in the compilers include path, which you can learn more about in the Compilation Process. The difference between the quoted and angle bracketed syntax is not supremely important, but it is discussed in the Compilation Process.

Whenever the compiler encounters the #include directive, it simply copies the contents of the file at the location of the directive. It is a direct copy; no changes are made to the version inserted into your code whatsoever.

This is typically used in the context of header files, as including a header file like this copies in forward declarations for functions, making them available for you to use.

However, this isn’t limited to strictly headers, though. You could also, for example, import a list of numbers into your code. Take file numbers.txt:

1, 2, 3, 4, 5, 6, 7, 8

In our code, we could write:

int numbers[] = {
#include "numbers.txt"
};

and this would be completely valid.

#define and macros

The #define directive is a little more complicated than the previous. It essentially allows you to define a symbol (in essence, a variable name), that whenever the compiler encounters the symbol, it text replaces it with what the symbol is defined as.

For example, if I am using the number pi in my project in many places, I don’t necessarily want to have to type out 3.141592..... every time I want to use it. However, storing pi in memory at runtime is a waste of memory. Instead, I could define pi as a macro:

#define PI 3.1415926f

Then, in my code, I can reference pi whenever I want:

float circumfrence = 2 * PI;

When the compiler sees the above line, it recognizes that PI is a defined macro, and it performs a text replace with the value of the macro. This has the bonus of saving the teeny bit of memory of storing pi in a runtime variable, since now the value is baked in to the code itself.

In fact, this is how the actual C math library provides pi to you!

ALternatively, macros can also take parameters, like functions. For example, if I have to print sets of 2 integers to the screen at a time, but I don’t want to type printf a bunch, I could define a custom symbol that does it for me:

#define INT_PRINTF(a, b) printf("%d, %d", a, b)

Then, whenever I use this macro in my code, I can provide it with two integers, and those will get filled in for a and b. It is important to note though that the tect replacement happens at compile time, so there is no actual INT_PRINTF function that you can reference anywhere. It purely exists as a convenience for you to use as a shorthand.

Macros can also be defined from the compiler comand line (see compiler syntax). This is very useful for turning program features on or off, and is used frequently in many scenarios, like below.

#if, #ifdef

Another common use for macros is for conditional compilation. Take the case when you are making a program designed to work on both Windows and Linux. There may be some code in your program that is platform dependent, i.e. some section of code depends on APIs that differ between the two operating systems (an example might include graphics, or networking, etc.).

You dont want to have to have two different versions of your code for the different systems, especially if the problematic section is only a few lines.

By using #if and #ifdef, you can detect if a macro is present, then only compile specific sections.

For example, Windows will automatically define the _WIN32 macro in all files, and Linux compilers will automatically define the __linux__ macro.

So, if I had some platform-dependent code, I could write something like this:

<some code here>

#ifdef _WIN32
<windows code here>
#endif

#ifdef __linux__
<linux code here>
#endif

<some code here>

You can see each section of platform code is wrapped in an ifdef/endif pair, and these sections are only activated if the corresponding macro has been defined. You can check for specific values of a macro, and do other, fancier boolean statements, but this simple definition-checking is very common.

There are a lot more variants of conditional macros, and they all accomplish similar things, but at the end of the day they all boil down to either enabling or disabling some section of code.

You can even stick other directives like includes or defines inside these blocks too, so you could conditionally include a header, or define a macro differently depending on some external variables.

Another place conditionals are used like this is something like microcontroller libraries, like the STM32 HAL. The HAL is designed to be usable on many, if not all of STs microcontrollers, and so they need a way to detect what MCU you’re compiling for, and enable specific sections of code that pertain to that MCU.

If you look at the sources and headers for the HAL, you will see a lot of this kind of macro usage.

Other directives

There are many other directives available for use, but they are not nearly as relevant or well used as the ones above.

For example, the #pragma once directive can act as an alternative to header guards, and #warning and #error can be used to intentionally emit a warning or error at a specific line of code, with a specific message.