The first phase of the compilation process is the "pre-processing" phase. This consists of scanning in the program text all the preprocessor directives, i.e. lines that begin with a "#" character, and executing the instructions found in there before presenting the program text to the compiler.
We will interest us with just two of those instructions. The first one is the "#define" directive, that instructs the software to replace a macro by its equivalent. We have two types of macros:
Parameter less. For example:
#define PI 3.1415
Following this instruction, the preprocessor will replace all instances of the identifier PI with the text "3.11415".
Macros with arguments. For instance:
#define s2(a,b) ( (a*a + b*b) /2.0)
When the preprocessor finds a sequence like:
s2(x,y)
It will replace it with:
x*x + y*y)/2.0 )
The problem with that macro is that when the preprocessor finds a statement like:
s2(x+6.0,y-4.8);
It will produce :
(x+6.0*x+6.0 + y+6.0*y+6.0) /2.0 )
What will calculate completely another value:
(7.0*x + 7.0*y + 12.0)/2.0
To avoid this kind of bad surprises, it is better to enclose each argument within parentheses each time it is used:
#define s2(a,b) (((a)*(a) + (b)*(b))/2.0)
This corrects the above problem but we see immediately that the legibility of the macros suffers. quite complicate to grasp with all those redundant parentheses around.
An "#undef" statement can undo the definition of a symbol. For instance
#undef PI
will erase from the pre-processor tables the PI definition above. After that statement the identifier PI will be ignored by the preprocessor and passed through to the compiler.
The second form of pre-processor instructions that is important to know is the
#if (expression)
. program text .
#else
. program text .
#endif
or the pair
#ifdef (symbol)
#else
#endif
When the preprocessor encounters this kind of directives, it evaluates the expression or looks up in its tables to see if the symbol is defined. If it is, the "if" part evaluates to true, and the text until the #else or the #endif is copied to the output being prepared to the compiler. If it is NOT true, then the preprocessor ignores all text until it finds the #else or the #endif. This allows you to disable big portions of your program just with a simple expression like:
#if 0
.
#endif
This is useful for allowing/disabling portions of your program according to compile time parameters. For instance, lcc-win32 defines the macro __LCC__. If you want to code something only for this compiler, you write:
#ifdef __LCC__
. statements .
#endif
Note that there is no way to decide if the expression:
SomeFn(foo);
Is a function call to SomeFn, or is a macro call to SomeFn. The only way to know is to read the source code. This is widely used. For instance, when you decide to add a parameter to CreateWindow function, without breaking the millions of lines that call that API with an already fixed number of parameters you do:
#define CreateWindow(a,b, . ) CreateWindowEx(0,a,b,.)
This means that all calls to CreateWindow API are replaced with a call to another routine that receives a zero as the new argument's value.
It is quite instructive to see what the preprocessor produces. You can obtain the output of the preprocessor by invoking lcc with the -E option. This will create a file with the extension .i (intermediate file) in the compilation directory. That file contains the output of the preprocessor. For instance, if you compile hello.c you will obtain hello.i.
|