The C Preprocessor is just about essential. It’s probably possible to write C without it, but I wouldn’t want to try.
The preprocessor supports:
- file inclusion – recursively
- conditional compilation of chunks of text
- macro definitions and usage
- some handy ‘constants’
The preprocessor is covered lightly in chapter 4 of K&R and tersely but comprehensively in Appendix A12.
I will cover the easy parts first – leaving macros for last.
All preprocessor directives begin with #. This makes them easy to spot.
In contrast, macro usage is not distinguished in any syntactic way. That can be a problem because it is not possible to distinguish between a function call and a parameterized macro expansion from syntax alone. More below.
#include #include "filename or relative path" #include MACRONAME
#include <stdio.h> /* searches system directories */ #include "foo.h" /* searches locally first, then system directories */ #include <sys/fcntl.h> /* searches 'sys' subdir of system directories for 'fcntl.h' */ #include FOO /* expands FOO as a macro then does one of the above - FOO should include " or <> delimiters */
Just remember that file inclusion is recursive, so if foo.h includes some files, then they will be included as well.
This is pretty simple except that the recursive expansion will often result in files being included multiple times. You must protect against this by creating a conditional compilation guard clause – explained below.
The conditional compilation directives are
#if numeric-expression /* true if 'numeric-expression' is not 0 */ #elif numeric-expression /* true if 'numeric-expression' is not 0 */ #endif #ifdef macro-name /* true if 'macro-name' is defined */ #ifndef macro-name /* true if 'macro-name' is NOT defined */
These guys do what you expect them to do.
The define preprocessor operator takes a macro name as an argument and evaluates to TRUE or FALSE (non-zero or 0) if the macro is defined or not. Thus ifdef and ifndef can be written as
#if defined macroname /* equivalent to #ifdef macroname */ #if ! defined macroname /* equivalent to #ifndef macroname */
Guarding against multiple inclusions of files is simple:
- enclose all the code in a conditional statement dependent on a unique macro name
- define the unique macro name inside the conditional
The pattern is
#ifdef __FOO__ #define __FOO__ important stuff #endif /* __FOO__ */
I like to tack a comment on the terminating #endif so I know what’s being terminated. This is a convention which always works. Some compilers allow you to tack the actual macro name w/o enclosing it in comment delimiters, but I’m old fashioned.
Finally we get to macros. And this can get a bit weird.
Simple things first: constants. C doesn’t support named constants. Instead cpp supports non-parameterized macros which look like comments but aren’t.
#define FOO "foo is not a real word" #define PI 3 /* in keeping with some arcane, misguided law */
When I write some C which includes the identifier FOO, the compiler never sees FOO. What it sees is the string which FOO is defined as. The preprocessor re-writes the text by substituting the string “foo is not a real word” (including the quote marks) for the token FOO.
This is distinctly different from a constant in – say – Ruby – which the ruby compiler does see and evaluate.
I’m going to repeat this again: The Compiler NEVER sees macros. Macros are evaluated to text strings and the text replaces the macro call.
Macros can also be parameterized – and this is where things get interesting – in the sense of ‘gee, that’s an interesting looking thing in the soup – what is it?’
#define muller(x, y) x * y foo = muller(1, 2); /* expands to foo = 1 * 2; */ foo = muller(1 + 2, 3 - 5); /* foo = 1 + 2 * 3 - 5; */ foo = muller(foo++, foo + 1); /* foo = foo++ * foo + 1; */
Here we have an innocent little macro to replace simple multiplication of two quantities by a silly name. It works in the simple case, but for the second two – not so much.
The right way to write this is:
#define muller(x, y) (x) * (y) foo = muller(1, 2); /* expands to foo = (1) * (2); */ foo = muller(1 + 2, 3 - 5); /* foo = (1 + 2) * (3 - 5); */ foo = muller(foo++, foo + 2); /* foo = (foo++) * (foo + 2); */
The inclusion of parentheses fixes the problem with second case, but the third case – well, you shouldn’t write this code this way anyway because you don’t know the order of evaluation of foo++ and foo on the right side of the assignment.
There are more rules – but this is the nut of writing macro definitions.
Just remember that macro expansion results in text which is fed to the compiler and make sure the text is completely unambiguous. Then everything will be fine – mostly.
When in doubt, compile with an option which produces the macro expansion and read what you get. (that’s the -E option for gcc)
There are a bunch of predefined constants which are very handy for writing debugging code. You use them like any other variable and the preprocessor evaluates them.
Here are two of my favorites:
- __LINE__ – the current line number
- __FILE__ – the path to the current file
As usual – go to K&R for a complete list – as of the original ANSI spec.
And this is it . . .
This is about it for this introduction to the C Programming Language.
Just a reminder – this series is intended to be a quick start to actually learning the language. I personally like working through K&R for that – the examples and problems are quite good.
For a more definitive – and relatively affordable – language definition, take a look at C – A Reference Manual by Harbison and Steele.