In C or C++ programs, any line that begins with the '#' character is known as a compiler directive. Basically, the compilation has the following stages:
In the preprocessing stage, the preprocessor goes through the source code, stripping comments and processing the compiler directives. In the compilation stage, the compiler will compile the preprocessed code into binary object files. In the linking stage, the linker will link the various object files together with the required libraries into the final executable.
The simplest compiler directive is the #define directive:
#define MAXSIZE 10
This directive simply defines the MAXSIZE symbol to the string "10", and whenever the preprocessor encounters the symbol MAXSIZE, it simply replaces it by the string "10". For example:
int array[MAXSIZE];
will become the following after preprocessing:
int array[10];
Note that we can actually define a symbol without specifying the substitution string:
#define WIN32
You can undefine a symbol that was previously defined by using the #undef directive:
#undef WIN32
The substitution string can be an expression, like the following:
#define THREE (1 + 2)
So you can have something like:
cout << THREE << endl;
But when defining expressions, it is always best to enclose them in parentheses. Consider the following example:
#define THREE 1 + 2 // dangerous cout << (2 * THREE) << endl;
The output is 4, not 6. The reason is because after substitution, we get:
cout << (2 * 1 + 2) << endl;
and expression evaluates to 4, because multiplication has higher precedence than addition.
It is also possible to define a symbol that takes in parameters, so it resembles a function:
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
In C or C++ parlance, such #define directives are also known as macros. These are useful when writing a full function for it is an overkill because it is just a one-liner but typing out the entire one-liner each time is an overkill. Besides, such macros can improve the readability of the code.
To use the MAX macro, we can use it like the following:
x = MAX(y, z);
Notice that I have parenthesized the parameters a and b in the substitution string. The reason is similar to the dangerous example above. I leave it as an exercise for the reader to figure out what value does the variable x hold when execution reaches the end of the following code snippet:
#define MUL(a, b) (a * b) // dangerous x = MUL(2 + 4, 6);
You can check whether a symbol is defined by using the #ifdef ... #endif directive. Like the C and C++ if statement, you can optionally add the #else clause.
#ifdef WIN64 const int numbits = 64; #else /* WIN64 */ const int numbits = 32; #endif /* WIN64 */
The above example means that if the symbol WIN64 is defined, then the line "const int numbits = 64;" will be compiled but not the line "const int numbits = 32;"; conversely, if WIN64 is not defined, then we will set the constant numbits to 32 instead.
Note that we can include more than one line between #ifdef and #endif, and the lines can include other compiler directives. Further, the #ifdef directives can be nested.
#ifdef WINDOWS #ifdef WIN64 #define NUMBITS 64 #else /* WIN64 */ #define NUMBITS 32 #endif /* WIN64 */ #endif /* WINDOWS */
There is also a #ifndef compiler directive that tests whether a symbol is not defined. The usage is similar to the #ifdef directive, and an example for #ifndef will appear shortly.
The next directive to be introduced here is one you should have seen many times already: the #include directive. Basically, the preprocessor will replace the #include directive with the contents of the included file in its place.
Suppose we have a file foo.h:
int foo() {
return 1;
}
and a bar.cpp:
#include "foo.h"
int main() {
int x = foo();
}
then when the preprocessor processes the file bar.cpp, the resultant file that gets passed to the compiler looks something like this (perhaps with differences in whitespace):
int foo() {
return 1;
}
int main() {
int x = foo();
}
For includes of files in the C or C++ library, we use angle brackets in place of the quotes surrounding the filename. Two examples:
#include <iostream> #include <stdio.h>
However, there could be a problem when headers are included multiple times. This can occur when there are two or more #include directives to the same file, or more indirectly by a file A.cpp including files B.h and C.h, and file B.h including file C.h again. Say the file A.cpp is:
#include "B.h"
#include "C.h"
int main() {
B b;
C c;
b.say();
c.say();
}
The contents of file B.h is:
#include "C.h"
class B : public C {
public:
void say();
};
The contents of file C.h is:
class C {
public:
void say();
};
However, when we try to compile A.cpp, the compiler complains that the class C is being redefined. The reason is that the preprocessor actually hands the compiler the following:
class C {
public:
void say();
};
class B : public C {
public:
void say();
};
class C {
public:
void say();
};
int main() {
B b;
C c;
b.say();
c.say();
}
So the compiler sees two copies of the definition of the class C and refuses to compile. (However, multiple forward declarations for the same function is okay.)
To overcome this, we can use the #ifndef and #define directives in C.h, as follows:
#ifndef __C_H__
#define __C_H__
class C {
public:
say();
};
#endif /* __C_H__ */
Please verify for yourself that after preprocessing, the compiler only sees one copy of the definiton of class C. Of course, it would be a good idea to apply the same trick on B.h as well. In case you are wondering, the __C_H__ symbol is derived from the filename: capitalize the filename, replace the dot by an underscore, and add two underscores to the front and the back. To say the truth, it does not matter how you derive the symbols, so long the derivation is consistent and allows each filename to be mapped to a unique symbol.
In fact, this is a very common technique seen in many header files.
Whew! This is quite a long miniature. I hope that you have learnt something useful from it.