Originally published by Robert Beisert at fortcollinsprogram.robert-beisert.com

Linux + C – Manipulating the Precompiler

Ever wondered what the whole #include thing really means? Of course you have.

Precompiler

There are basically three stages that gcc goes through when it compiles your program: precompilation, translation, and linking. At this point, we’re interested in that first stage.

The precompiler is the program which reads and organizes your C code for the translator to convert into object code. It does a number of things, such as making sure all your functions actually exist, checking whether you declared your variables properly, expanding variables, and many more. Frankly, the study of the compiler is a worthy challenge in itself.

Sometimes (most of the time), the precompiler needs a bit of help. It doesn’t know where to find functions like printf(), gets(), or pow(), because you didn’t define them in your source. If it doesn’t know where those functions are, it can’t perform its job. This is where #include comes in – it tells the precompiler where to go to get information on these functions.

In general, a line starting with a # (hash) sign is interpreted by the precompiler, and ignored by everything else.

There are several very common precompiler directives we employ when working in C:

#include SOURCE – use this source when preparing the code

#define TAG VALUE– in the following code, translate the TAG into the following VALUE before translation

#ifndef TOKEN – if there is no definition for the token (i.e. there is no #define TOKEN in the previously interpreted sources), do ______

#ifdef TOKEN – if there is a definition for the token, do _____

#if STATEMENT – if the precompiler determines that the statement is true (using only basic logic and precompiler directives), do _______

#else – if the previous if / ifdef / ifndef statement is interpreted to be false

#endif – close out the if-else chain

Why would we use precompiler directives instead of basic code? Basically, it does the following things when working with multiple sources:

  • It makes sure we don’t define the same functions and global variables more than once.
  • It creates constants across all sources. If we want all our sources to use the same BUFFER_SIZE, we can #define it once and forget about it.
  • It lets us include other sources in the compilation process simply. Otherwise, we have to create LONG gcc calls, which include every header and code file we’ll use in this program.
  • It lets us switch between two sets of code immediately. We can create test copies of our code (complete with print statements) alongside our final code, swapping between them by changing #if 0 to #if 1.

I’m sure you can find other excellent reasons, as well. These are just my personal favorites.

precomp.c

Normally, you wouldn’t use #ifdef or #ifndef this way, but it works as a teaching tool.

Note how we don’t use semicolons for precompiler directives.


#include <stdio.h>

#ifndef __PANDAS__
#define PANDAS 28
int write_phrase()
{
printf("We used #define to create a meaning for PANDAS\n");
return 0;
}

#endif

#if 0

I can write whatever I want here. Unless I use &quot;&quot; or '', because the interpreter still cares whether or not it all matches.
The precompiler will edit it all away before it handles the code itself.

#endif

#ifndef PANDAS
#define VALUE 22

#else
#define VALUE 15

#endif

#ifdef PANDAS

int main(int argc, char *argv[])
{
int val = VALUE;
write_phrase();
printf("Using our code, we find the value of VALUE to be: %d\n", val);
return 0;
}

#endif