9. The C Preprocessor – C Programming Essentials

Chapter 9. The C Preprocessor

The C preprocessor is a program that processes a C source file before the compiler translates the program into object code. During processing the C source file, it performs certain modifications on the file based upon instructions or directives to the preprocessor. The preprocessor directives are simply commands that start with a # symbol (character). We will examine these directives one by one in this chapter.

Macro Substitution

We have already seen that the #define preprocessor directive is used to create a symbolic constant for use in a program. The general form of this directive is given by:

#define      macro-name replacement—text

where macro-name is replaced with replacement–text through the program.

For example, the directive

#define      PI     3.1415

directs the preprocessor to replace all succeeding symbolic constant PI with the text 3.1415.

The #define directive can also be used for defining parameterized macros. A parameterized macro is a macro with arguments. The general form of defining such macros is:

#define     macro_name(arg1,arg2,...)      replacement_text

Some examples of parameterized macros are:

#define     read(i)      scanf("%d",&i)
#define     print(i)     printf("%d\n",i)
#define     getchar()    getc(stdin)

The last example is a macro with zero parameters. Notice that the arguments that are used at the time of defining a parameterized macro are formal parameters. So, a two-level replacement is done during the macro substitution.

First, the formal parameters are substituted by actual parameters, and then the macro body is substituted. For example, the statement:

print(x);

will be replaced by

printf("%d\n",x);

Parentheses in parameterized macros play a great role in macro substitution. Consider the classic example of squaring a number by parameterized macro:

#define    square(n)   (n)*(n)
     ---
     ---
number=10;
sqrnum=square(number);

Because of this macro substitution, the preprocessor produces the following code to the compiler:

number=10;
sqrnum=(number)*(number);

Hence, the value of sqrnum that will be produced is 100. But if we drop the parentheses in the macro definition, it will look like:

#define     square(n)  n*n

Now, if we write:

number=10;
sqrnum=square(number+1);

the code that will be produced to the compiler is:

number=10;
sqrnum=number+1*number+1;

This will yield the value of sqrnum as:

10+1*10+1=21

However, this is not the desired value. So, the parentheses in this macro definition are necessary and we need to be careful while writing such macros.

Even when the parentheses are given properly, there may be side effects. It generally occurs when certain operations are performed with parameterized macros. If we write:

sqrnum=square(number++);

this will expand to:

sqrnum=(number++)*(number++);

If the value of number is 10 prior to invoking the macro, this will generate the value of sqrnum as:

sqrnum=10*11
      =110

though the expected value is 100.

Properly formed parameterized macros save a lot of key pressings while entering the source code and also improve the readability of the program.

There is a #undef directive that may be used to ‘undefine’ a defined macro. For example:

#define     RESULT     15
      ---
      ---
#undef      RESULT

File Inclusion

Most of the programs in this book used the #include directive to include header files. The general form for this directive is:

#include     <name_of_file>

Another form for using the directive is:

#include     "name_of_file"

In the former case, the left(<) and right(>) angular brackets mean that the search for name_of_file is done in some implementation-defined manner. For example, in UNIX systems, the name_of_file is searched for in the directory /usr/include.

In the latter form, the angular brackets are replaced with double-quotation marks. Here, the search for name_of_file begins in the directory where the source file is located. Usually, it is the present working directory. The details are implementation-dependent, though.

Conditional Compilation

The C preprocessor recognizes a variety of conditional compilation directives, and they are listed together with their meanings in Table 9.1.

The general form for the #if directive is

#if     expr

where expr is a constant expression. For example,

#if HEALTH==1
      #define ACTIVITY "good"
#else
      #define ACTIVITY "bad"
#endif

Table 9.1. Conditional Preprocessor Directives

Directive

Meaning

#if

conditional if

#else

else

#elif

else-if

#endif

end of if and/or elif

#ifdef

if defined

#ifndef

if not defined

Here, if the symbolic constant HEALTH is 1, the symbolic constant ACTIVITY is defined as “good”; otherwise, it is defined as “bad”. This construct is very similar to the if statement. The #endif tells the preprocessor about the end of #if. The #elif is similar to the else_if syntax of C statements. For example,

#if STATUS == 1
      #define PERSON "Single"
#elif STATUS == 2
      #define PERSON "Married"
#elif STATUS == 3
      #define PERSON "Divorced"
#elif
      #define PERSON "Widowed"
#endif

Additional Directives

The #line preprocessor directive has the general form:

#line linenumber "filename"

and is used for diagnostics in debugging. Consider that the C source file myprog.c is of the form:

#include <myheader.h>
int main()
{
     ---
     ---
     ---
}

Assume that this source is having 20 lines and the header file myheader.h consists of 30 lines. Now, if there is an error at the 14th line of myprog.c, the compiler will issue a message that an error has occurred at line 34, and not at line 14, since the myheader.h is included. But if we insert a #line directive as

#line 1

in-between #include directive and main(), we find the message of error at line 14. Instead of the above #line directive, if we write

#line 17 "myprog.c"

this will instruct the compiler to treat the following line as number 17, and to issue any errors as originating in the file myprog.c.

The #error directive may be used to display a message, normally an error message. It has the general form as:

#error message

where message is a string literal. For demonstration choose the following:

#if !defined(MUL) && !defined(DIV)
   #error "multiplication and division operator not specified"
#endif

The #pragma directive has the form:

#pragma action_string

The action caused by a #pragma is compiler implementation-dependent. If the compiler does not understand the instruction in a #pragma line, it ignores the line.

Predefined Preprocessor Identifiers

There are a number of reserved predefined identifiers in ANSI C. These are for use by the preprocessor and are fixed, and cannot be undefined by #undef directive. These are presented in Table 9.2

Table 9.2. Predefined Preprocessor Identifiers

Identifier Name

Purpose

Value Type

_LINE_

current line number in source file

integer

_FILE_

name of current source life

string of character

_DATE_

compilation date

string of the form “Feb 16, 1996”

_TIME_

compilation time

string of the form “13:09:27”

_STDC_

evaluates to a non-zero integer, conforming to ANSI

integer

ANSI Standard Header files

We present here most of the standard header files together with a brief description about the content of the header files.

assert.h

This header file contains a macro named assert(). It may be used to test an expression at the execution time. When an erroneous expression is given as the argument to assert macro (e.g., assert (<erroneous expr>) ), on evaluation the macro refers a logical false value (i.e., zero) and the program is terminated and, a system-dependent message is displayed as shown below:

assertion failed: <erroneous expr>, file <filename>, line <line no>
Abnormal program termination

In case of correct expression, the program continues normally.

ctype.h

The ctype.h header file contains a number of character-testing and character-conversion macros – for example, isdigit(), isalnum(), toupper(), and so on.

errno.h

The errno.h header file defines some symbolic constants. These constants are used in error processing.

float.h

The way a floating-point number is represented by the compiler is specified in the float.h header file. Basically, it defines symbolic constants that represent floating-point precision, maximum and minimum floating-point values, etc.

limits.h

The limits.h header file holds the information about the range of values that different data types can assume. Actually, it defines symbolic constants that tell the minimum and maximum values for a char, an int, a long, a float, a double, and other data types.

math.h

The content of math.h header file is various mathematical function prototypes and macros. These are used by different mathematical functions.

signal.h

There are some exceptional conditions that arise at the time of interrupt processing and error processing. The means for copying with such special conditions are provided within signal.h.

stdarg.h

When there is a need to process functions with variable numbers of arguments, we need to include stdarg.h header file.

stddef.h

The compiler’s own functions use the variables and macros defined within stddef.h header file.

stdio.h

The stdio.h header file is included whenever any sort of input–output is done in a program. It contains several function prototypes, macros, and all necessary declarations to perform program input–output.

stdlib.h

The C standard library provides several utility functions. These utility functions use the function prototypes and macros that are in the header file stdlib.h.

string.h

This header file contains the necessary function prototypes and macros used for string processing in a C program.

time.h

The function prototypes, macros, and typedefs are contained in the header file time.h for those functions related to time and dates.

Summary

In general, a header file is included in a program when the program needs one or more function prototypes, macros, etc., which are already within that header file.

New Terminology Checklist

Macro

Pre-processor

ANSI

Header

Directives

Arguments

Exercises

1.

What, in general terms, is the role played by the C preprocessor?

2.

Which symbol always precedes preprocessor directives?

3.

Where on the line, must all preprocessor directives begin?

4.

It should be avoided to use #define for long sequences of code that can be implemented as a function — Do you agree with the above statement? Explain.

5.

Justify the statement — Separate header files for different set of declarations improves information hiding.

6.

How can we write a generic macro to swap two values?

7.

Write a program to print an array of integers that defines and uses a macro ARRAYPRINT, say. The macro receives the array and the size of the array as its arguments.

8.

Define a macro ARRAYSUM to get the sum of values in a numeric array.

9.

Is it acceptable for one header file to #include another?

10.

Does the sizeof operator work in preprocessor #if directives?