munotes®

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

DirectiveWhat it does
#includeInserts another file here
#defineDefines a macro: a name, or a name with parameters
#undefRemoves a definition
#ifdef, #ifndefInclude the following text only if a name is or is not defined
#if, #elif, #else, #endifInclude the following text depending on a constant expression
#lineChanges the line number the compiler reports
#errorStops compilation with a message
#pragmaImplementation-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:

HeaderWhat 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-text

The 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 cent

There 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-text

No space between the name and the opening bracket. #define SQUARE (x) ... defines an object-like macro whose text begins (x).

munotes.in104

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.50

Work through the two failures by substitution, which is exactly what the preprocessor did.

  • BAD_SQUARE(2 + 1) becomes 2 + 1 2 + 1. Precedence groups that as 2 + (1 2) + 1, which is 5.
  • 100 / BAD_SQUARE(5) becomes 100 / 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 1

SQUARE(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

MacroFunction
Handled byPreprocessorCompiler
Type checkingNoneFull
Argument evaluatedOnce per appearance in the bodyExactly once
Code sizeOne copy per useOne copy in total
Call overheadNoneA call, which inline can remove
Can be debuggedPoorly: the source line is the callYes
Works for any typeYes, which is its one real advantageOne set of types
Can recurseNoYes
munotes.in105

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 compiled

The 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 */

#endif

The 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.

munotes.in106

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 text substitutes text. No type, no storage.
  • #define NAME(p) text is 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()) calls next twice.
  • Prefer a function; use a macro for constants, conditional compilation and type-independent code.
  • #ifdef tests definedness; #if tests 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 */
#endif

6. 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.

munotes.in107

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.

munotes.in108

The rest of this subject

These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.

Report or request
Done!