Tutorial Introduction to C Language
Preprocessor Directives
All of the examples in this tutorial series so far have contained just one source code file. You have used the compiler to compile this source code into an executable program. In reality however, any software engineering project will be made up of many files organised into logical groups according to the design of the code repository. These files will have dependencies between them. In other words, one file may need another to exist before it can be compiled. Or it may need some data that is declared in another file in order to work properly. This kind of dependency naturally leads to some organisational overhead. The software engineer must keep track of which files require which, and what the dependency graph looks like. This is even more pertinent when working within a team of engineers all working on the same project. The C language provides some useful directives that can help you organise your file structures and dependencies. These directives are actioned before the compiler compiles the code inside the files. This stage is typically known as the preprocessor. The function of the preprocessor is to look for various directives in your code and alter the source code to either include some other text or source or to substitute some text.
In general all of the preprocessor directives start with the hash symbol, # followed by the directives, and then followed
by the text to be substituted. Remember, this happens before the compiler stage!
Line Splicing
This is a handy feature when you are dealing with very long lines of text in your program. You can use multiple short lines and splice them
together as though they were one long line. To do this place a backslash character, \, at the end of the line. Before
compilation begins this will be removed along with the newline character at the end so the compiler sees one long line instead. This
can be very useful when you have long strings of text in your code. In the following example the long verse is spread over multiple lines
but the compiler will just see one long line, and hence print one long line to the console.
// *******
// line splicing
// *******
{
//
// in the following lines the '\' and the newline character at the end of the line
// will be removed so it will look like one long line to the compiler
char some_text [] = {"Venus the lady of love inflameth the heart. . . " \
"then Cupid her son shooteth his dart, and being blind, " \
"some times striketh with his arrow of love, and some time with " \
"his arrow of hatred, but at all times so shooteth that his " \
"arrows tend to love, in the beginning, though afterwards " \
"discords arise betwixt lovers."};
printf ("%s\n", some_text);
} /* scope */
#include
The #include directive is usually followed by a file name either in between two double quotes, "file name", or between two
brackets, <file name>. Everything in the file will be substituted in place of the line that contains the #include.
This is what the compiler will see. As far as the compiler is concerned there is no difference between including the file text or actually having
the text in the file itself. This is very handy as it allows us to break up our files and organise them into meaningful groups and relationships.
The quoted version of #include is normally used for our own project files, or files that are part of an
Application Programming Interface (API) similar to that provided by the MLC framework. The bracketed version of #include
is normally used for system or internal header files.
We have already seen an example of the latter form at the top of our example code files, #include <stdio.h>. The contents of the
internal header file stdio.h will appear in place of the #include directive. This header file is required because it contains
the declarations of the functions used for input and output. The stdio stands for standard input output and is required
whenever we use functions such as printf. There are many other internal header files we can use that declare handy functions such as
<errno.h>, <math.h>, <stdlib.h>, <locale.h>, <string.h>, <threads.h>, and many more.
/* we need this in order to use the printf() function */
#include <stdio.h>
/* include our example of a header file */
#include "./some_header.h"
/*!
* \brief Main program and starting point.
*/
int
main (
int argc,
char* argv[]
)
{
You can include one C source file inside another one. You may have some functions or code snippets defined inside it. However, I would urge that
you refrain from this type of use for #include as it will make your code very difficult to follow and debug. Below is an example of what
I would consider to be a bad use of the #include directive. If you need to organise your code across multiple C source files then you can
keep them independent from each other, and then link them together after they
have been compiled. For a complete example take a look at
main_preprocessor_1.c and
poor_include.c.
// *******
// #include
// *******
{
/*
* Example of a bad way to use #include files - it makes your code very difficult
* to follow.
*
* The contents of poor_include.c will be substituted in to this line.
*/
#include "poor_include.c"
}
#define and #undef
The #define directive is usually followed by an identifier and then followed by some sequence of text or tokens. After the
#define has been declared, then wherever the preprocessor finds the identifier it will substitute it for the sequence of words
or tokens. The code example below shows the VALUE_OF_PI identifier declared as a text string of 3.14.
Later on when the preprocessor encounters the VALUE_OF_PI identifier, it will remove it and substitute the text 3.14.
When the compiler comes to compile the code it will not see the VALUE_OF_PI identifier, it will see the text 3.14 and
it will compile it into a float value. Hence it is equivalent to using the text 3.14 directly.
// *******
// #define and #undef
// *******
// after this declaration any part of the source code that contains VALUE_OF_PI
// will be substituted for 3.14
// NOTE that the VALUE_OF_PI is NOT a variable it is a straight substitution.
// short approximation of pi
#define VALUE_OF_PI 3.14
//
printf ("value of pi from the DEFINE: %f\n", VALUE_OF_PI);
// this is equivelent to to following
printf ("value of pi from a constant: %f\n", 3.14);
//
// Once an identifier has been used you can not use it again.
// uncomment this line to see the compiler error, then comment it out again
//#define VALUE_OF_PI 3.14159
// If you want to change the value of the identifier then you must undefine it first
// forget the value of VALUE_OF_PI
#undef VALUE_OF_PI
// Now you can assign a new value to the identifier
// longer value of pi
#define VALUE_OF_PI 3.14159
//
printf ("value of pi from the DEFINE: %f\n", VALUE_OF_PI);
// this is equivelent to to following
printf ("value of pi from a constant: %f\n", 3.14159);
At this point you may be wondering what is the point of using the #define in the first place? Why not just use the 3.14 directly?
The answer to this is about organising your code to be flexible. Let us assume you have used the value of pi 30 times in your project and it is distributed
around lots of your source code files. Now let us say you need a more precise value of 3.14159. In order to make this change you will need to search
all of your source code files for all 30 instances of the value of pi. If on the other hand you had used the #define instead, then you just need
to change the value of this identifier once, and recompile your code. This is a much easier and better way to organise your code. In fact, I usually keep
a separate header file that contains all of my the #define all together, say in a header file called something like mlc_constants.h.
Then I can update the values of these types of constant values whenever I need to, and all I have to do is recompile the project afterwards.
Sometimes it is also useful to group together compiler or platform specific data or settings, say in a header file called something like mlc_platform.h.
This latter header file raises an interesting point. How can we distinguish which compiler we are using and on which platform? The answer now follows.
#if, #ifdef, #elif and #else
At some point in our project we may need to perform some conditional compiling. That means we may wish to compile some code differently depending
on some conditions. A typical use case for this is if we compile the code on multiple platforms. For instance on Windows, Linux and Mac. Whilst
these platforms all adhere to the C language specification, they will no doubt have their differences. We can address this with conditional compilation, and
we can use the #if and #ifdef preprocessor directives to do so. The logic follows a similar path to that of the
boolean operators covered before. However take note, there is one
very key difference. The boolean operators are compiled by the
compiler into expressions that will be used during the program execution, whilst the directives covered in this section control how the
code is compiled and so do not get processed when the program is running.
// *******
// #if #ifdef #elif #else
// *******
//
// The following conditional compilations will compile different code
// depending on which platform it is being compiled on.
#ifdef _WIN32
// defined for Windows platforms
printf ("I am running on a Windows platform!\n");
#elif __linux__
// defined for Linux platforms
printf ("I am running on a Linux platform!\n");
#elif __APPLE__
// defined for Apple platforms
printf ("I am running on an Apple platform!\n");
#else
// everything else not Windows, Mac or Linux
printf ("I am running on some other platform!\n");
//
// NOTE: This scope terminator at the end of the if block
#endif
Note The compiler specific to a given platform will define the inbuilt directives of _WIN32 for Windows,
__linux__ for Linux, and __APPLE__ for Apple products.
#ifndef
Another very important use case for these directives is as a header file lock to make sure the contents of a header file are only seen once
by the compiler. The MLC project uses this technique in all of its header files. Please see the code example below for details. The technique
uses a #ifndef directive followed by some unique name. I usually make up something that I know will be specific and descriptive to the
header file in questions, _SOME_HEADER_H_, and unlikely to be used anywhere else in the project. It means that if _SOME_HEADER_H_
has not been defined then the text body of the file all the way to the #endif will be seen by the compiler. To ensure this only happens
once during compilation we place a #define of the unique identifier _SOME_HEADER_H_ inside the code block. The effect of doing
this is to lock out any repeat #includes of this header file. Examine the code below and make sure you are happy with how this logic works.
If you have not guessed it already, the #ifndef means only add this body of text if the identifier _SOME_HEADER_H_ is not
defined, and if it is defined then skip it. For a complete example take a look at
some_header.h.
Normally, I would skip all the verbose comments around this technique. However, they are here so that you can follow the logic.
/* the ifndef only includes the body of this file if the identifier _SOME_HEADER_H_ has not been used before */ #ifndef _SOME_HEADER_H_ /* _SOME_HEADER_H_ is now defined and so any further includes to this file will stop at the ifndef line */ #define _SOME_HEADER_H_ /* some generic function to do something */ int some_function_A (int a, int b); /* another generic function to do something */ float some_function_B (float a, float b); /* this marks the end of the scope for the ifndef for _SOME_HEADER_H_ */ #endif /* _SOME_HEADER_H_ */
You will find all of the code located here $MLC_HOME/src/c/tutorial_introduction_to_c. Open files called
main_preprocessor_1.c,
poor_include.c,
some_header.h
and try experimenting with them. Do not be afraid to break them ... Enjoy, and have a Hacktastic time!!!