In this article we will introduce the preprocessor and covered the basics, including object- and function-like macros, the include directive, conditional compilation, and end with the two special directives #error and #pragma.
The #include directive
The most straight-forward preprocessor directive is #include. When the preprocessor finds this directive it simply opens the file specified and inserts the content of it, as though the content of the file would have been written at the location of the directive.
It can take two forms, for example:
#include <systemfile.h>
#include "myfile.h"
The first is used to include standard headers like stdio.h. The latter is for your own application-specific headers.
Macros
One of most useful features of the preprocessor is to allow the user to define macros, which simply is an identifier that is mapped to a piece of source code. Whenever the preprocessor finds the macro in the application source code it replaces the macro with the definition.
Basically, there are two types of macros, object-like macros and function-like macros, the difference is that function-like macros have parameters.
By convention, macro names are written using upper-case only. The only exception is when a macro is used to replace something that should have been a function but is implemented using a macro for the sake of efficiency.
The directive #define can be used to define a macro. In the following example, we define NUMBER_OF_PLAYERS to the constant "2". This is an object-like macro.
#define NUMBER_OF_PLAYERS 2
int current_score[NUMBER_OF_PLAYERS];
Here, the function-like macro PRINT_PLAYER is mapped a more complex piece of source code.
#define PRINT_PLAYER(no) printf("Player %d is named %s", no, names[no])
The definition of a macro should, technically, be specified on a single source line. Fortunately, the C standard allows you to end a line with a backslash and continue on the next physical line. For example:
#define A_MACRO_THAT_DOES_SOMETHING_TEN_TIMES \
for (i = 0; i < 10; ++i)\
{ \
do_something(); \
}
In addition to the macros that a user can define, the C standard specifies a number of predefined and library-provided macros that could be used. For example, the macro __FILE__ contains the name of the current source file, as a string.
If you ever would like to make a macro name undefined you can use the #undef directive.
Object-like macros
Object-like macros can be used to replace an identifier in the source code with some kind of replacement source code.
Typically, macros can be used to declare a constant that could be configured in one location. Also, constants could be used to make the source code more readable, even if the value is not intended to change. For example:
#define SQUARE_ROOT_OF_TWO 1.4142135623730950488016887
double convert_side_to_diagonal(double x)
{
return x * SQUARE_ROOT_OF_TWO;
}
double convert_diagonal_to_side(double x)
{
return x / SQUARE_ROOT_OF_TWO;
}
Preprocessor macros could be used for very weird things, since all that the preprocessor does is to replace an identifier with an arbitrary piece of source code. For example, the following is legal code (although, you will probably have to answer to your boss if you ever try to write something like this):
#define BLA_BLA );
int test(int x)
{
printf("%d", x BLA_BLA
}
Function-like macros
Function-like macros are macros that take parameters. When you use them they look like a function call. A function-like macro could look like the following:
#define SEND(x) output_array[output_index++] = x
When the preprocessor finds a function-like macro in your application source code it will replace it with the definition. The parameters of the macro will be inserted into the resulting source code at the location of the formal parameter variables.
So, if you write the following:
SEND(10)
Then the compiler will see:
output_array[output_index++] = 10
In the second part of this article we will revisit function-like macros and discuss some of the traps that are easy to fall into.
Conditional compilation
One of the most powerful features of the preprocessor is the so-called conditional compilation—this means that portions of the code could be excluded in the actual compilation under the certain conditions.
This means that your source could contain special code for, say, the ARM processor. Using conditional compilation, this code could be ignored when compiling for all other processors.
The preprocessor directives #ifdef, #ifndef, #if, #elif, and #else are used to control the source code. The#ifdef (#ifndef) directive includes a section if a preprocessor symbol is defined (undefined). For example:
#ifdef ARM_BUILD
__ARM_do_something();
#else
generic_do_something();
#endif
The #if directive can handle any type of integer and logical expression, for example:
#if (NUMBER_OF_PROCESSES > 1) && (LOCKING == TRUE)
lock_process();
#endif
The #elif directive works like a combined #else and #if.
#if and #elif can use the special operator defined to check if a symbol is defined. This is useful in combination with complex tests, for example:
#if defined(VERSION) && (VERSION > 2)
...
#endif
In part two of this article we will revisit conditional compilation and discuss whether you should prefer #if:s or #ifdef:s in your application.
Include guards
One typical use for conditional compilation is to ensure that the content of include files are only seen once. This will not only speed up the compilation, but also ensures that the compiler will not issue an error (e.g. for redeclaration of a struct) if the header file is included twice.
An include guard typically looks like the following:
#ifndef MYHEADER_H
#define MYHEADER_H
/* The content of the header file goes here. */
#endif
Clearly, the first time the header file is included the symbol MYHEADER_H is not defined and the content is included in the compilation. The second time the header file is read the symbol is defined and the content is excluded.
Error directives
The #error directive can be used to generate a compiler error message. This is useful when performing consistency checks, for example:
#if USE_COLORS && !HAVE_DISPLAY
#error "You cannot use colors unless you have a display"
#endif
The #pragma directive
Another preprocessor directive is #pragma. This directive allows the programmer to control the behavior of the compiler and gives compiler vendors the opportunity to implement extensions to the C language.
The #pragma directice is not covered further in this article since it has little to do with the main task of the preprocessor.