Previous: 3.3.1 The C Preprocessor
Up: 3.3 Coding Programs for Readability
Next: 3.3.3 Including Header Files
Previous Page: 3.3.1 The C Preprocessor
In Chapter we introduced the define compiler directive
which defines symbolic names for strings of characters.
Such a string of characters can be arbitrary, for example a sequence of
characters representing a numeric constant. These names can then be used
anywhere in the program instead of the string itself.
The C preprocessor replaces these symbolic names with
the specified strings prior to compiling the program.
We have seen examples where
using names for arbitrary
strings makes it easy to change all occurrences of these names by merely
changing the definitions.
It also makes for easier reading and
debugging of programs by allowing the programmer to use a name which has
some meaning rather than some ``magic number''.
The definition is called a macro and the preprocessor performs a macro expansion when it substitutes the string for the name. A macro definition takes the form:
#define PI 3.14159
#define SIZE 1000
#define RSQUARED radius * radius
#define AREA PI * RSQUARED
#define LONG This is a very long macro
definition we continued to the next line
When directives such as these appear in the source file,
then the macros are said to have been defined.
We have defined macros for the symbols PI, SIZE,
RSQUARED, AREA and LONG.
With the above definitions, the defined names may be used anywhere in program
statements. The preprocesser generates the expanded source code by
string replacement, for example:
Original code Expanded code after preprocessingAs can be seen, the preprocessor replaces the macro name with the specified replacement string in the entire source file following the definition. The substitution is not made if a macro name, occurs in double quotes as in the format string in the printf() statement shown above.circum = 2 * PI * radius; circum = 2 * 3.14159 * radius; y = x + SIZE; y = x + 1000; printf("SIZE = ",SIZE); printf("SIZE = ",1000); AREA; 3.14159 * radius * radius;
The scope of the macro definition is the entire source file following the definition line. The definitions may be removed at any point in the program by a directive #undef, for example:
#undef SIZEThe above directive makes the preprocessor ``forget'' the previous definition for SIZE. If desired, a new definition may be specified for SIZE at this point. It is a common practice to put macro definitions at the top of the source file, unless the old definitions are removed at some point in the source file and new definitions are specified:
#define SIZE 40 /* SIZE is define to be the string 40 */
...
#undef SIZE /* SIZE is undefine */
#define SIZE 100 /* SIZE is defined to be 100 */
Identical definitions for identifiers may appear in a file without causing any
problems; however, two different definitions for an identifier represent an
error.
#define SIZE 40
#define SIZE 40 /* OK */
#define SIZE 100 /* ERROR */
The only way to make a new definition for an identifier is to first undefine
it, i.e. remove its first definition.
Macro definitions may also have formal parameters which are replaced by the actual arguments given in the macro call. This is similar to parameters in function calls; however, macro arguments are treated as strings of characters and are substituted for parameters by the preprocessor; no evaluation takes place. Consider the example:
#define READ_FLT(fvar) scanf("%f", &fvar)
The macro encapsulates the expression for reading a float number,
i.e. a macro call is replaced by a string that represents
a correct scanf() function call to read a
float number into an object passed to the macro.
The actual argument in a macro call replaces fvar
in the replacement string.
In other words,
every time the macro is called, the expanded code is
substituted literally except that fvar
in the definition is replaced by the
argument given in the actual call. Here are some examples of macro calls with
parameters together with the expanded code:
macro call Expanded CodeMacro calls in these cases expand to C statements. Such calls are said to expand to in-line code, because the resulting code represents statements in the source code. These types of macro calls can be used in place of function calls, for example, instead of writing a function to square a number, we can define a macro:READ_FLT(x); scanf("%f", &x); READ_FLT(rate); scanf("%f", &rate);
#define SQ(x) (x * x)We can use such a macro in any expression, e.g.,
y = SQ(radius);
printf("Square of %d is %d\n", radius, SQ(radius));
However, remember, macro calls are substitutions,
and macro parameters are neither
evaluated nor checked for data type consistency.
Therefore, proper placement of
parentheses is important in macro definitions. For example, consider the
following macro call and expanded code:
SQ(x+y)expanded becomes
(x + y * x + y)The expanded code is not the square of (x + y), as we would expect. By precedence rules, it is a sum of three terms, x, y * x, and y. A proper definition of a macro for square should be:
#define SQ(x) ((x) * (x))With this definition,
SQ(x+y)will expand correctly to
((x + y) * (x + y))Here is a simple example program:
/* File: macro.c */
#define READ_FLT(fvar) scanf("%f", &fvar)
#define PI 3.14159
#define SQ(x) ((x) * (x))
main()
{
float radius;
printf("Type Radius: ");
READ_FLT(radius);
printf("Area of a circle with radius %6.2f is %6.2f\n",
radius, PI * SQ(radius));
}
The output of a sample run is:
Why use macros with arguments when functions will serve the same purpose? The advantage is practical, NOT logical. When a function is called, there is a certain amount of run time overhead, i.e. extra time needed during execution. The overhead comes from passing arguments, transferring control, returning a value, and returning control. If a function is called just a few times, the overhead is negligible. However, if a function is used numerous times, e.g. in a loop executed many times, then the overhead can become significant.
A macro on the other hand has no run time overhead. It is expanded at compile time into in-line code which has no overhead at run time. If execution time for a program is a problem because of a frequently used routine, then writing a macro for that routine makes good sense, as long as the operation can be simply expressed as a macro.
Let us look at another example program to make use of these new facilities.
Read a set of high temperature readings for some number of days and to count the number of nice days, bad days, and the average temperature for the period. Nice days are those days whose temperature falls within some ``comfort zone''.
The high level algorithm for this task is straight forward;
prompt the user and read first temperature
while there are more days to read
process one day's temperature
accumulate total temperature
read the next temperature
print results
With this algorithm, we next consider what information
we will be working with in this program.
We read daily temperatures, so we will need a variable for that,
and variables to count the number of nice and bad days.
Since we compute the average temperature, we
accumulate the total of all the daily temperatures,
so we need a variable for that.
Next we consider how we will implement the algorithm using functions
to hide details.
For example, the step to print results, printing the number of nice
and bad days as well as computing and printing the average temperature
can be done in a function, print_results(), which is
given the number of nice days, bad days, and the cumulative total
of temperatures.
The step of processing one day's temperature is another candidate;
however, this step involves updating our counts of nice and bad days.
Since, as we have seen, functions cannot access variables local to main,
we refine our algorithm to fill in some of the details of this step:
prompt the user and read first temperature
while there are more days to read
if it's a nice day, count a nice day
otherwise count a bad day
accumulate total temperature
read the next temperature
print results
We can use a function to test if a day is nice, thus hiding the details
of this operation.
We are now ready to write the code for main() as shown in Figure
3.9.
It should be noted we have made an additional design decision here; we use a zero value for the temperature read in as the loop termination. Also not that we have provided prototype statements for our functions, nice_day() and print_results(). This is sufficient information about these functions when considering the logic of main(). (We have specified the return value of print_results as type int, but the function has no real meaningful return value).
We next turn out attention to the function, nice_day(). This function is given the temperature and should return True if this qualifies as a nice day, and False otherwise. The task specified that the temperature of a nice day is to fall within some ``comfort zone'', i.e. not too cold and not too hot. We can write the algorithm for this function from this information:
if temperature is too cold, return False
if temperature is too hot, also return False
otherwise, this is a nice day, return true
We choose to implement the too cold and too hot tests using macro:
#define TOO_COLD 80 #define TOO_HOT 90Coding of the function is straight forward. Similarly, for print_results(), the algorithm is:#define HOT_DAY(t) ((t) > TOO_HOT) #define COLD_DAY(t) ((t) < TOO_COLD)
print number of nice days and bad days
if there are any days counted
compute the average temperature
print the average temperature
The resulting code for these functions is shown in Figure 3.10.
Notice, we have used the type void for the function
print_results().
We will describe this type in more detail later, but it indicates
that the function does not return any value. When execution reaches
the end of the function body, it simply returns with no value.
Compiling and executing this program with some sample data produces the following sample session: