The C Preprocessor
Chapter Twenty-Two
Syllabus topic 2, "Type of operators: Arithmetic operators, relational and logical operators, Increment and Decrement operators, assignment operators, the conditional operator, Assignment operators and expression, Precedence and order of Evaluation Block Structure, Initialization, C Preprocessor"
Pages 104 to 108 of 222
In one line
The preprocessor is a program that edits your source text before the compiler sees it, driven by lines beginning with #, and it understands no C at all.
The directives
| Directive | What it does |
|---|---|
#include | Inserts another file here |
#define | Defines a macro: a name, or a name with parameters |
#undef | Removes a definition |
#ifdef, #ifndef | Include the following text only if a name is or is not defined |
#if, #elif, #else, #endif | Include the following text depending on a constant expression |
#line | Changes the line number the compiler reports |
#error | Stops compilation with a message |
#pragma | Implementation-defined instruction to the compiler |
A directive occupies its own line, begins with #, and takes no semicolon. Adding one puts a stray semicolon into your program wherever the name is used.
#include
Two forms, and the difference is where the file is looked for.
#include <stdio.h> /* the system's include directories */
#include "mystuff.h" /* this file's own directory first, then the system's */The rule in practice: angle brackets for the standard library and anything installed on the machine, double quotes for headers that are part of your own program.
The headers you will use this semester:
| Header | What it declares |
|---|---|
<stdio.h> | printf, scanf, getchar, putchar, fgets, FILE, EOF |
<stdlib.h> | abs, atoi, malloc, exit, rand |
<string.h> | strlen, strcpy, strcmp, strcat, strstr |
<math.h> | sqrt, pow, fabs, floor, ceil, round, sin |
<ctype.h> | isdigit, isalpha, toupper, tolower |
<limits.h> | INT_MAX, INT_MIN, CHAR_BIT |
<float.h> | DBL_DIG, FLT_MAX |
<stdbool.h> | bool, true, false |
Object-like macros
#define NAME replacement-textThe preprocessor replaces every occurrence of NAME below that line with the replacement text. The name is conventionally in capitals so a reader can see it is a macro.
#include <stdio.h>
#define MAX_STUDENTS 60
#define PI 3.14159
#define GREETING "Welcome to FY B.Sc. IT"
#define PERCENT 100.0
int main(void)
{
printf("%s\n", GREETING);
printf("a class holds %d students\n", MAX_STUDENTS);
printf("a circle of radius 3 has area %.5f\n", PI * 3 * 3);
printf("45 out of 60 is %.2f per cent\n", 45 * PERCENT / MAX_STUDENTS);
return 0;
}Welcome to FY B.Sc. IT
a class holds 60 students
a circle of radius 3 has area 28.27431
45 out of 60 is 75.00 per centThere is no type and no storage. MAX_STUDENTS is not an int; it is the two characters 6 and 0. Chapter 10's table compares #define with const and says why const is usually better.
Function-like macros, and the brackets
#define NAME(parameters) replacement-textNo space between the name and the opening bracket. #define SQUARE (x) ... defines an object-like macro whose text begins (x).
The C Preprocessor
Now the part that matters. A macro is text substitution, so the text is dropped into the middle of whatever expression surrounds it, and precedence then applies to the result.
#include <stdio.h>
#define BAD_SQUARE(x) x * x
#define GOOD_SQUARE(x) ((x) * (x))
#define BAD_HALF(x) x / 2
#define GOOD_HALF(x) ((x) / 2.0)
int main(void)
{
printf("BAD_SQUARE(3) = %d\n", BAD_SQUARE(3));
printf("BAD_SQUARE(2 + 1) = %d <- wanted 9\n", BAD_SQUARE(2 + 1));
printf("GOOD_SQUARE(2 + 1) = %d\n", GOOD_SQUARE(2 + 1));
printf("100 / BAD_SQUARE(5) = %d <- wanted 4\n", 100 / BAD_SQUARE(5));
printf("100 / GOOD_SQUARE(5) = %d\n", 100 / GOOD_SQUARE(5));
printf("BAD_HALF(7) = %d <- integer division\n", BAD_HALF(7));
printf("GOOD_HALF(7) = %.2f\n", GOOD_HALF(7));
return 0;
}BAD_SQUARE(3) = 9
BAD_SQUARE(2 + 1) = 5 <- wanted 9
GOOD_SQUARE(2 + 1) = 9
100 / BAD_SQUARE(5) = 100 <- wanted 4
100 / GOOD_SQUARE(5) = 4
BAD_HALF(7) = 3 <- integer division
GOOD_HALF(7) = 3.50Work through the two failures by substitution, which is exactly what the preprocessor did.
BAD_SQUARE(2 + 1)becomes2 + 1 2 + 1. Precedence groups that as2 + (1 2) + 1, which is 5.100 / BAD_SQUARE(5)becomes100 / 5 5. Left to right, that is(100 / 5) 5, which is 100.
Two sets of brackets, always. Round each parameter where it appears, and round the whole body. ((x) * (x)) survives both attacks.
The second macro trap: an argument evaluated twice
#include <stdio.h>
#define SQUARE(x) ((x) * (x))
int calls = 0;
int next(void)
{
calls = calls + 1;
return calls;
}
int main(void)
{
calls = 0;
int result = SQUARE(next());
printf("SQUARE(next()) gave %d, and next() ran %d times\n", result, calls);
printf("a FUNCTION would have called it once and given 1\n");
return 0;
}SQUARE(next()) gave 2, and next() ran 2 times
a FUNCTION would have called it once and given 1SQUARE(next()) expands to ((next()) * (next())), so the function runs twice and multiplies two different numbers. The same happens with SQUARE(i++), which then also modifies i twice in one expression and is undefined behaviour. A real function cannot do this, and that is the strongest argument for writing one.
Macro against function
| Macro | Function | |
|---|---|---|
| Handled by | Preprocessor | Compiler |
| Type checking | None | Full |
| Argument evaluated | Once per appearance in the body | Exactly once |
| Code size | One copy per use | One copy in total |
| Call overhead | None | A call, which inline can remove |
| Can be debugged | Poorly: the source line is the call | Yes |
| Works for any type | Yes, which is its one real advantage | One set of types |
| Can recurse | No | Yes |
The C Preprocessor
Prefer a function. Use a macro for a constant, for conditional compilation, and for the rare case where the same code must work for several types.
Conditional compilation
#include <stdio.h>
#define DEBUG 1
int main(void)
{
#if DEBUG
printf("debug: the program has started\n");
#endif
printf("doing the real work\n");
#ifdef DEBUG
printf("debug: DEBUG is defined, whatever its value\n");
#endif
#ifndef RELEASE
printf("RELEASE is not defined\n");
#endif
#if DEBUG > 5
printf("this line is not compiled at all\n");
#else
printf("DEBUG is 1, so this branch was compiled\n");
#endif
return 0;
}debug: the program has started
doing the real work
debug: DEBUG is defined, whatever its value
RELEASE is not defined
DEBUG is 1, so this branch was compiledThe text in a branch that is not taken is not compiled. It is removed before the compiler sees it, so it need not even be valid C, and an error in it will not be reported. That is what makes conditional compilation useful for code that only works on one operating system, and it is also why a mistake inside a switched-off branch can sit there for years.
#ifdef NAME tests whether the name is defined, not whether it is true. #define DEBUG 0 followed by #ifdef DEBUG takes the branch. Use #if DEBUG when you mean the value.
The standard predefined macros, which are always available and are genuinely useful:
#include <stdio.h>
int main(void)
{
printf("this is line %d of file %s\n", __LINE__, __FILE__);
printf("the C standard in use is %ld\n", __STDC_VERSION__);
return 0;
}this is line 5 of file lines.c
the C standard in use is 201710__FILE__ prints whatever name the file was compiled under. That listing was saved as lines.c, so that is what it printed; on your machine it will be the name you chose. __LINE__ is 5 because the printf is the fifth line of the file, counting the #include as line 1.
The include guard
A header included twice would declare everything twice. The standard protection is a macro that records that the file has been seen:
#ifndef STUDENT_H
#define STUDENT_H
/* the contents of the header */
#endifThe first time, STUDENT_H is not defined, so the body is included and the macro is defined. The second time, the body is skipped. Every header in the standard library does this, and you should do it in any header of your own.
What this does NOT mean
The preprocessor does not understand C. It has no idea what a type, an expression or a scope is. It moves text.
A macro is not a constant. It has no type and no address. const gives you one that does.
A macro is not a function. No type checking, no single evaluation of arguments, and no recursion.
The C Preprocessor
A directive does not take a semicolon. #define N 10; puts the semicolon into the program: int a[N]; becomes int a[10;];.
#ifdef does not test a value. It tests whether a name is defined at all.
Text in an untaken branch is not checked. It is removed, errors and all.
#include does not import a library. It inserts a text file of declarations. The library code is joined on by the linker, which is chapter 5.
Quick revision
- The preprocessor edits text before compilation, driven by lines starting with
#, and takes no semicolon. #include <...>for system headers,#include "..."for your own.#define NAME textsubstitutes text. No type, no storage.#define NAME(p) textis function-like, and needs no space before the bracket.- Bracket every parameter and the whole body:
#define SQUARE(x) ((x) * (x)). - A macro argument is evaluated once per appearance in the body, so
SQUARE(next())callsnexttwice. - Prefer a function; use a macro for constants, conditional compilation and type-independent code.
#ifdeftests definedness;#iftests a value.- Untaken conditional branches are removed before compilation and are not checked.
- Include guard:
#ifndef X/#define X/ body /#endif. __LINE__,__FILE__and__STDC_VERSION__are always defined.
Test yourself
1. What does #define SQ(x) x*x give for SQ(3+1), and why?
3+13+1, which is 7. The text is substituted and then precedence applies. Write ((x) (x)).
2. What is wrong with #define MAX 100;?
The semicolon is part of the replacement text, so int a[MAX]; becomes int a[100;]; and does not compile.
3. Give two differences between a macro and a function.
A macro is expanded as text by the preprocessor with no type checking; a function is compiled once and its arguments are type-checked. And a macro may evaluate an argument more than once, while a function evaluates each argument exactly once.
4. #define DEBUG 0 then #ifdef DEBUG. Is the branch taken?
Yes. #ifdef asks whether the name is defined, not what its value is. Use #if DEBUG to test the value.
5. Write an include guard for a header called student.h.
#ifndef STUDENT_H
#define STUDENT_H
/* contents */
#endif6. How many times does next() run in SQUARE(next()) where SQUARE(x) is ((x) * (x))?
Twice, because x appears twice in the body and each appearance is replaced by the argument text.
7. Why is #define SQUARE (x) ((x)*(x)) wrong?
The space before the bracket makes it an object-like macro whose replacement text is (x) ((x)*(x)). Remove the space.
The C Preprocessor
What can be asked on this, and how to answer it
"What is the C preprocessor? List and explain its directives." Define it as a text-processing stage that runs before the compiler, driven by lines beginning with #, understanding no C. Give the table of directives with one line each, and note that no directive takes a semicolon.
"Explain #define with examples of both kinds of macro." Give an object-like macro and a function-like one, then spend the answer on the bracket rule, with SQ(3+1) giving 7 as the worked failure. That example is what the question is really for.
"Distinguish between a macro and a function." Give four rows of the table: preprocessor against compiler, no type checking against full, an argument possibly evaluated several times against exactly once, and code repeated at each use against one copy called. Conclude that a function is preferable unless the code must work for several types.
"What is conditional compilation? Why is it used?" Including or excluding source text at compile time with #if, #ifdef, #ifndef, #else, #elif and #endif. Used for code specific to one machine or operating system, for debugging output that can be switched off, and for include guards. Add that untaken text is removed before the compiler sees it.
"What is an include guard and why is it needed?" A #ifndef / #define / #endif wrapper round a header, so that including the header twice declares its contents once. Without it, a second inclusion would redeclare everything and the compilation would fail.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.