SITERAW

Preprocessor Directives

After those exhaustive chapters on pointers, arrays, and strings, it's time for a little breather. What I mean is — you've been through quite a bit in the last few chapters, and there's no way I'd deny you a chance to catch your breath as we wrap up this third part of our C course.

That said, no way we're resting without learning something new and exciting (are you kidding?). So we're diving into a short and simple chapter — think of it like a refresher. This one's all about the preprocessor, the program that runs right before compilation.

Don't let its simplicity fool you: what we cover here is super useful. It's just... easy to grasp — and hey, that works out nicely for us :)

The #include Directive

As I explained way back in the early chapters, source code often contains special lines called preprocessor directives.

These directives all have one thing in common: they always start with the # symbol. That makes them pretty easy to spot.

The first (and so far, only) directive we've seen is #include.

As mentioned earlier, this directive lets you insert the contents of one file into another. It's especially used to include .h files, whether they're from standard libraries (stdlib.h, stdio.h, string.h, math.h...) or your own custom .h files.

To include a .h file located in your IDE's library folder, use angle brackets < >:

#include <stdlib.h>

To include a .h file from your own project folder, use double quotes:

#include "myfile.h"

Here's how it works: the preprocessor kicks in before compilation. It scans all your files, hunting for those lines that begin with #.

When it finds a #include, it literally replaces that line with the full content of the file you pointed to.

Let's say I've got a file.c that holds the code for some functions, and a file.h that has the prototypes for those same functions. When you write #include, the contents of file.h are dropped straight into file.c, exactly where the #include "file.h" line appears.

Imagine our file.c looks like this (some of you may recognize these functions from a previous chapter):

#include "file.h"

int main() { return 0; }

void attackPokemon(int pokeHP, char pokeStatus) { /* Function code */ }

int catchPokemon(int pokeNumber) { /* Function code */ }

And file.h looks like this:

void attackPokemon(int pokeHP, char pokeStatus);
int catchPokemon(int pokeNumber);

When the preprocessor does its thing — right before file.c gets compiled — it pastes the contents of file.h into file.c. So right before compilation, file.c ends up looking like this:

void attackPokemon(int pokeHP, char pokeStatus);
int catchPokemon(int pokeNumber);

int main() { return 0; }

void attackPokemon(int pokeHP, char pokeStatus) { /* Function code */ }

int catchPokemon(int pokeNumber) { /* Function code */ }

The contents of the .h file slide right into place where #include was :)

It's not too hard to grasp — I bet a bunch of you already figured that's how it worked.

At least now, with this extra bit of explanation, I hope everyone's clear ;)

The #include directive simply inserts one file into another. That's all. But it's important to understand that fully.

So why put function prototypes in .h files instead of just dumping them at the top of .c files?

Well, it's mostly a matter of structure and good practice. Technically, you could just put prototypes at the top of your .c files (and for tiny programs, some people do). But for better organization, it's strongly recommended to keep them in .h files.

The #define Directive

Now let's take a look at a new preprocessor directive: #define.

This directive lets you define a preprocessor constant. In other words, it allows you to associate a specific value with a word. Remember when we used the number 03 to display a ♥? Well, with #define you could write:

#define HEART 03

Or, another example:

#define INITIAL_LIVES 3

The structure is simple:

  • Start with #define
  • Follow it with the name you want to associate
  • Then the value it should stand for

But be careful — despite how it looks (and the habit of writing them in all caps), this is very different from the constants we've used up to now, like:

const int INITIAL_LIVES = 3;

Those constants take up space in memory. Even if the value never changes, your 3 still gets stored somewhere. That's not the case with preprocessor constants :)

So how does it work? Basically, #define tells the preprocessor to replace every instance of the word with its assigned value. Think of it like the "find and replace" tool in Word ;)

So for example, this line:

#define INITIAL_LIVES 3

...will go through your file and replace every INITIAL_LIVES with 3.

Here's a .c file before the preprocessor steps in:

#define INITIAL_LIVES 3

int main(int argc, char *argv[]) { int lives = INITIAL_LIVES;

/* Some code... */ }

And here's how it looks after preprocessing:

int main(int argc, char *argv[])
{
    int lives = 3;

/* Some code... */ }

So before the compilation phase, all #define values are swapped out by the preprocessor. The compiler only ever sees the modified version, where the substitutions have already been made.

Why use this instead of regular constants like we've seen before?

Well, like I said earlier, they don't use any memory. That makes sense — since by the time it gets to the compiler, there are only numbers left in the code.

Another benefit: the replacement applies throughout the entire file where the #define appears. If you had used a memory-based constant inside a function, it would've only existed within that function, and then poof — gone. But #define applies across all functions in the file, which can be super handy when coding.

Want a concrete example of #define in action?

Here's one you'll likely use soon. When opening a window in C, it's common to define constants for the window's dimensions:

#define WINDOW_WIDTH  1280
#define WINDOW_HEIGHT 720

The beauty of this? If later on you decide the window's too small, just update the #define values and recompile.

Tip: #define values are usually placed in .h files next to your prototypes (in fact, if you peek into library headers like stdlib.h, you'll see some in there!).

#define constants are super accessible. Instead of digging through your code to find the line where you set the window size, you just tweak a line in the header. That's real time saved ;)

To sum up: preprocessor constants help you "configure" your program before compilation. It's like a mini configuration system, really ;)

Using #define to set array size

It's common to use #define to specify array sizes. For instance:

#define MAX_SIZE 1000

int main(int argc, char *argv[]) { char string1[MAX_SIZE], string2[MAX_SIZE]; // ... }

But wait... I thought you couldn't use a variable inside the brackets when declaring an array?

True — but MAX_SIZE isn't a variable ;) Like I mentioned, the preprocessor transforms the file before compilation into something like this:

int main(int argc, char *argv[])
{
    char string1[1000], string2[1000];
    // ...
}

...and that's perfectly valid :)

By defining MAX_SIZE this way, you can use it throughout your code. If 1000 turns out to be too small later on, just update the #define line, recompile, and off you go — all your arrays will now use the new size.

Doing Calculations in #define

Yep, you can even do simple calculations with #define. For example, this code creates a constant for WINDOW_WIDTH, one for WINDOW_HEIGHT, and another for TOTAL_PIXELS, which is just width × height:

#define WINDOW_WIDTH   1280
#define WINDOW_HEIGHT  720
#define TOTAL_PIXELS   (WINDOW_WIDTH * WINDOW_HEIGHT)

TOTAL_PIXELS gets replaced before compilation with (WINDOW_WIDTH * WINDOW_HEIGHT), which then becomes (1280 * 720), and that equals 921600 ;)

Always wrap your calculations in parentheses like I did above.

You can use any basic operators you know: addition +, subtraction -, multiplication *, division /, and modulo %.

Predefined Constants

Besides the constants you define yourself, the preprocessor also provides a few built-in constants. I'll be honest — I've personally never needed them, but you might find them useful someday, so here they are ;)

Each of them starts and ends with two underscores _:

  • __LINE__ : gives you the current line number
  • __FILE__ : gives you the name of the current file
  • __DATE__ : gives the date of compilation
  • __TIME__ : gives the time of compilation

They can be handy for error messages, like so:

printf("Error on line %d in file %s\n", __LINE__, __FILE__);
printf("This file was compiled on %s at %s\n", __DATE__, __TIME__);

Simple Definitions

You can also write something like this:

#define CONSTANT

...without assigning any value.

This tells the preprocessor that the word CONSTANT is defined. It doesn't hold a value, but it "exists."

What's the point of that!?

Okay, I get it — this one's not as obvious as the others. But believe me, there is a reason for it... and we'll discover it very soon. :)

Macro Directives

We've already seen how #define can tell the preprocessor to replace a word with a value. For example:

#define NUMBER 9

...means every instance of NUMBER in your code will be replaced with 9. As we saw, it's basically a simple "find and replace" that the preprocessor performs before compilation.

But guess what? I've got something new for you! :)

#define is actually even more powerful than we thought. It can also replace a word with... entire chunks of code! When we use #define to replace a word with code, it's called creating a macro.

Macro Without Parameters

Let's start with a super basic example:

#define HELLO() printf("Hello");

What's different here? Well, we added parentheses after the keyword (in this case, HELLO()). We'll see why that matters in just a bit.

Let's try using this macro in some code:

#define HELLO() printf("Hello");

int main(int argc, char *argv[]) { HELLO()

return 0; }

Result:

Hello

Okay sure, not very original for now ;)

The key thing to understand is that macros are really just code snippets that get dropped directly into your source code right before compilation.

So the code above actually becomes this when the preprocessor does its thing:

int main(int argc, char *argv[])
{
    printf("Hello");

return 0; }

If you understood that, then you've already grasped the basic idea behind macros :)

But wait... can we only have one line of code in a macro?

Thankfully, no — you can include multiple lines. You just need to put a backslash \ at the end of each line, like this:

#define TELL_YOUR_STORY()  printf("Hi, welcome to SiteRaw\n"); \
                           printf("The best site on the Internets\n"); \
                           printf("Where you can learn to code and stuff...\n");

int main(int argc, char *argv[]) { TELL_YOUR_STORY()

return 0; }

Result:

Hi, welcome to SiteRaw
The best site on the Internets
Where you can learn to code and stuff...

Notice in the main function that we don't end the macro call with a semicolon. Since the preprocessor sees it as a single instruction, no semicolon is needed at the call site.

Macro With Parameters

So far, we've only looked at macros without parameters — nothing between the parentheses. Those are mainly useful for shortening repetitive blocks of code.

But macros get really interesting when you add parameters. It works almost like a function.

#define IS_ADULT(age) if (age >= 18) \
                      printf("You are an adult\n");

int main(int argc, char *argv[]) { IS_ADULT(25)

return 0; }

Result:

You are an adult

You could even add an else to display "You are a minor." Give it a try yourself — it's not hard! Just don't forget that little backslash \ before each new line.

Here's the idea behind the macro:

#define IS_ADULT(age) if (age >= 18) \
                      printf("You are an adult\n");

We put in parentheses the name of a "variable" we call age. Anywhere in the replacement code where age appears, it'll be swapped out with whatever value you pass when calling the macro (in our example, 25).

So, right after preprocessing, our source code will look like this:

int main(int argc, char *argv[])
{
    if (25 >= 18)
    printf("You are an adult\n");

return 0; }

The macro call has been replaced with real code, and the value 25 has been directly plugged in.

You can also create macros with multiple parameters:

#define IS_ADULT(age, name) if (age >= 18) \
                            printf("You are an adult, %s\n", name);

int main(int argc, char *argv[]) { IS_ADULT(25, "Jace")

return 0; }

That pretty much covers the essentials of macros! Just remember: they're code replacements that can take parameters, which makes them more flexible than regular #define constants.

In most cases, you probably won't need to use macros often. However, some complex libraries — like SDL, for example — use macros a lot. So it's definitely worth getting familiar with them now, to avoid feeling totally lost later on ;)

Preprocessor Conditions

Hold onto your hats: you can actually write conditional logic in the preprocessor! 😄

Here's how it works:

#if condition1
    /* Code to compile if condition1 is true */
#elif condition2
    /* Else, if condition2 is true, compile this block */
#endif

The #if directive lets you insert a preprocessor condition. #elif stands for "else if." The condition ends when you write #endif. You'll notice — no curly braces needed with the preprocessor.

The big advantage here is that you can make your code compile conditionally. If the condition is true, the following code gets compiled. If not, it's simply stripped out before compilation. It won't even exist in the final executable.

#ifdef, #ifndef

Now let's talk about why you might want to #define a constant without giving it a value, like we saw earlier:

#define CONSTANT

This lets you use #ifdef, which means "if the constant is defined." There's also #ifndef, which means "if the constant is not defined."

That opens up possibilities like this:

#define WINDOWS

#ifdef WINDOWS /* Code for Windows */ #endif

#ifdef LINUX /* Code for Linux */ #endif

#ifdef MAC /* Code for Mac */ #endif

This is actually how cross-platform programs adapt to different operating systems :)

Of course, you still need to recompile the program for each OS (sadly, it's not magic 😄). If you're on Windows, you place #define WINDOWS at the top and compile. If you want to build the Linux version with its specific code, you change that line to #define LINUX, recompile, and now the Linux-specific section is the one that gets included — the others are ignored.

#ifndef to Prevent Infinite Includes

#ifndef is super common in .h files to avoid what's called infinite includes.

Wait — what's an infinite include??

Picture this. You've got two files: A.h and B.h. File A.h includes B.h, so far so good. But let's say B.h also includes A.h... This can totally happen in real code! One file depends on the other, and vice versa.

Now think about what would happen:

  • The compiler reads A.h and sees it needs to include B.h
  • It reads B.h, which says it needs to include A.h
  • So it includes A.h again, which says to include B.h again...
  • And so on. Forever.

Yeah... you don't need to be a programming wizard to realize that's not going to end well.

Eventually, the preprocessor will throw in the towel and complain: "That's enough includes already!" And boom — your compilation crashes 😜

So what's the secret weapon to avoid this nightmare? Here's the trick. From now on, structure your .h files like this:

#ifndef DEF_FILENAME // If the constant hasn't been defined, it's the first time the file is included
#define DEF_FILENAME // We define the constant so the file won't be included again

/* Contents of your .h file (includes, function prototypes, defines, etc.) */

#endif

Basically, you wrap everything in your .h file (your includes, prototypes, defines...) between a #ifndef and a #endif.

Let's make sure you fully understand how this condition works.

Say the .h file is included for the first time. The preprocessor checks: "Has DEF_FILENAME been defined yet?"

Since it hasn't, the condition is true, so it steps inside. And the very first line it hits is:

#define DEF_FILENAME

Now the constant is defined. So if the file ever gets included again, the #ifndef condition will be false, and the content will be skipped — no more double includes!

Of course, you can name the constant whatever you like. I personally go with DEF_FILENAME — just a habit of mine. Everyone has their little quirks ;) The important thing (and I really hope you catch this) is to use a different constant name in each .h file. If they all use the same name, only the first one will ever get included — the rest will be ignored. 😅

So just replace FILENAME with the actual name of your header file.

If you want proof I'm not just making this up, go have a look at the standard library .h files on your system. You'll see they all follow this exact structure — with a #ifndef at the top and a matching #endif at the bottom. That's how they make sure there are no infinite inclusion loops.

That wraps up this final chapter of Part III. Honestly, I kind of feel like I just taught you a whole new programming language! 😄

And in a way, I did! The preprocessor — that mysterious tool that reads your code before the compiler even sees it — has its own little language.

Next up, we'll get back to more classic C programming... starting with how to create your own variable types!

Ready to dive in?

Learn C Programing for Beginners

Enjoyed this C / C++ course?

If you liked this lesson, you can find the book "Learn C Programing for Beginners" from the same authors, available on SiteRaw, in bookstores and in online libraries in either digital or paperback format. You will find a complete C / C++ workshop with many exclusive bonus chapters.

More information