The Preprocessor
The goal of this lab is to deepen your familiarity with the C preprocessor and better understand the ways of parameterized macros.
- Topic A: Naming Constants
- Topic B: Conditional Code
- Topic C: Macros
- Topic D: Including Files
- Topic E: Why Use Macros
Topic A: Naming Constants
Exercise 1: Simple Definitions and Basics
of #include
-
Copy the C program
preprocess.c
to yourlabs
directory.#define SIZE 5 int main (void) { int i; int a[SIZE]; a[0] = 0; for ( i = 1 ; i < SIZE ; i++ ) { a[i] = i + 2*a[i-1]; } return 0; } // main -
Let's discover what the C preprocessor does with the simplest form
of
#define
. Type the following command and compare the output to that file. Be prepared to explain what the preprocesor has done.clang -E preprocess.c
Recall that the-E
flag simply tells the compiler to run the preprocessor and give the output. -
Right now, the program does not do much interesting. Let's add a
printf
statement. (Do not add the corresponding#include
yet.) Add the following before thereturn
statement.for (i = 0; i < SIZE; i++) printf ("a[%d] = %d\n", i, a[i]);
- What do you expect to have happen if you preprocess the new file? After answering, check your answer experimentally.
- What do you expect to have happen if you compile and run the program? After answering, check your answer experimentally.
-
Add the necessary
#include
directive to your program. - What do you expect to happen if you preprocess the revised file? After answering, check your answer experimentally.
- What do you expect to have happen if you compile and run the program? After answering, check your answer experimentally.
-
Replace the line that defines
SIZE
with#define SIZE 3+4
- What do you expect to happen if you preprocess the revised file? After answering, check your answer experimentally.
- What do you expect to have happen if you compile and run the program? After answering, check your answer experimentally.
-
Restore the
#define
with#define SIZE 5
Exercise 2: Stretch
Stand up and stretch briefly.
Exercise 3: Where Does That Semicolon Go, Anyway?
By now, you're probably used to putting a semicolon at the end of
every statement. You may have noted that we did not put a semicolon at the end
of our #define
directive. Let's try adding one.
-
Replace the
#define
in your programpreprocess.c
with#define SIZE 5;
- What do you expect to have happen if you now compile and run your program? After answering, check your answer experimentally.
- What do you expect to see when you preprocess the file? After answering, check your answer experimentally.
Exercise 4: More Ways to Define Constants
Let's explore other ways to think about associating values with names.
-
Remove the
#define
line from your programpreprocess.c
. - What do you expect to see when you preprocess the file? After answering, check your answer experimentally.
- What do you expect to have happen if you now compile and run your program? After answering, check your answer experimentally.
-
You can also define names on the compile command line
with
-DNAME=VALUE
. Let's try settingSIZE
to 10 with the following command.clang -DSIZE=10 -o preprocess preprocess.c
-
Run the newly compiled version of
preprocess
to see if we get the expected result. - Discuss with your group why this technique might be useful. Present your collective answer to the class leader or class mentor before moving on to the next exercise.
Exercise 5: Multiply-Defined Constants
-
Reinsert the following line at the top of your program.
#define SIZE 5
-
What do you expect to have happen when you try to compile the
program using the following command?
clang -DSIZE=10 -o preprocess preprocess.c
After answering, check your answer experimentally. -
As you no doubt have discovered, our C compiler doesn't
particularly like it when you try to define a constant twice. Can
we allow the programmer to specify a size during compilation if she
wants to, but use a default size if she does not specify one during
compilation? Certainly. We can tell the preprocessor to check
whether a constant is defined before defining it. Replace the
definition in your code with
#ifndef SIZE #define SIZE 5 #endif
-
Verify that the program uses a size of 5 when compiled in the
normal way, and a size of 10 when compiled
with
-DSIZE=10
.
Topic B: Conditional Code
Exercise 6: Testing Code
It can be quite helpful to print out messages in the middle of a program. For example, in looking at my binary search routine, I sometimes like to print out the values of the lower bound, upper bound, and midpoint.
-
Add the following declaration
to your program and add the following line to the end of the first for loopint numEvens = 0;
Finally, print the value ofif (a[i] %2 == 0) numEvens++;
numEvens
before thereturn
, like thus.printf ("Number of even numbers: %d\n", numEvens);
-
What do you expect the value of
numEvens
to be when you compile (leavingSIZE
at 5)? After answering, check your answer experimentally. -
Some people can spot the problems immediately, but others will find
that it's helpful to have the program trace itself. So let's add
the following line to end of the first loop.
printf ("i = %d, a[i] = %d, numEvens= %d.\n", i, a[i], numEvens);
- Now rerun the example. Have you spotted the problem?
- Correct the problems you've observed.
-
Okay, we've fixed the problem, so we want to drop the debugging
code. Usually, we comment it out. But that makes things hard. So
replace the
printf
with the following.#ifdef TESTING printf ("i = %d, a[i] = %d, numEvens= %d.\n", i, a[i], numEvens); #endif // TESTING
-
Compile the program (without setting the
TESTING
flag). Verify it runs without the test output. -
Now, suppose that we realize that there's another error. We want
to test again. Compile the program, setting the testing flag, as in
clang -DTESTING -o preprocess preprocess.c
ormake CPPFLAGS=-DTESTING preprocess
- Verify that the testing messages now reappear.
Topic C: Macros
Exercise 7: Maximal Macros
The definition of a "maximum" function should be fairly simple. In
the code below, we have used a slightly different name to remind
ourselves that maxf
is a function.
int
maxf (int x, int y)
{
return x > y ? x : y;
} // maxf
-
Write a test program using
maxf
that verifies that the larger of 7 and 11 is 11. (Yes, the answer is obvious, but it's good to check anyway.) While good practice dictates using separate files formaxf
and your test, put them all in one file anyway. Call your programmax1.c
. -
Often, as C programmers, we are tempted to turn such simple code
into a macro. Since macros are inlined, we don't need the return
and we can write the following.
Add this macro to#define MAXM(X,Y) X > Y ? X : Y
max1.c
. -
Replace the call to
maxf
with acall
toMAXM
. -
What do you expect to see when you run the preprocessor on
max1.c
? After answering, check your answer experimentally. -
When you run the program, do your expect the results from the
revised
max1.c
to differ? If so, how? After answering, check your answer experimentally.
Exercise 8: A Squared Detour
Your reading cautions us about a danger implicit in the following directive.
#define SQUARE(X) X*X
Let's explore that danger.
-
Create a program,
squarem.c
, that uses this macro to compute the square of 5. Print the result. Do you get the output you expect? -
Modify your program so that it instead computes the square of
2+3
. That is, use
Do you still get the result you expect?SQUARE( 2+3 )
-
You should have discovered that you get a very different result
than you expected. Why? Let's use the preprocessor to find out.
What does the call to
SQUARE
turn into in the following?clang -E squarem.c
-
Given your analysis from the previous step, fix the definition of
SQUARE
.
Exercise 9: More Manipulations of Maxima
Let's try a few more examples to make sure that our maxf
function and MAXM
macro work correctly. Here are a
few tests to add to your list.
/* Tests for a three-way maximum.
* Precondition: pNumErrors has valid address for an integer
* Postcondition: Increments the integer pointed to by pNumErrors if the
* largest of a, b, anc is not equal to expected.
*/
void
check3 (int a, int b, int c, int expected, int * pNumErrors)
{
int result = maxf (maxf (a, b), c);
if (result != expected)
{
printf ("For max (max (%d, %d), %d), expected %d, got %d.\n",
a, b, c, expected, result);
(*pNumErrors)++;
}
} // max3
int
main (void)
{
int numErrors = 0;
// Check various permutations
check3 (1, 2, 3, 3, &numErrors);
check3 (1, 3, 2, 3, &numErrors);
check3 (2, 1, 3, 3, &numErrors);
check3 (2, 3, 1, 3, &numErrors);
check3 (3, 1, 2, 3, &numErrors);
check3 (3, 2, 1, 3, &numErrors);
return numErrors;
} // main
-
Determine whether or not
maxf
successfully meets these additional tests. -
Determine whether or not
MAXM
successfully meets these additional tests. (Note that you'll need to replace each instance ofmaxf
incheck3
withMAXM
). -
As your reading suggests, one reason we see problems like the
preceding is that textual substitution can raise issues of
precedence in the subsituted expression. They give a somewhat
different definition of
MAX
, using lots and lots of parenthesis to guarantee that there is no ambiguity.
Verify that this definition passes the tests.#define MAXM(X,Y) ((X) > (Y) ? (X) : (Y))
Exercise 10: The Danger of Semicolons, Revisited
As we saw earlier, it can be dangerous to put a semicolon at the end of a constant definition. Should it make a difference if we put one at the end of a macro definition?
What do you expect to have happen if you add a semicolon to the
definition of MAXM
given above? After answering,
check your asnwer experimentally.
Exercise 11: Even More Fun with Macros
The reading raises an issue with side effects and macros. Let's explore that concern.
-
What value do you expect
a
,b
, andc
to have in the following expression? Recall that++var
adds one tovar
and gives back the new value ofvar
.int a = 5; int b = 7; int c = maxf (++a, ++b);
- Check your answer experimentally.
-
We perhaps might expect
MAXM
to give the same results. Determine experimentally whether or not it does. - Explain went wrong in this case.
- How might you repair this problem?
Topic D: Including Files
Exercise 12: The Structure of Headers
Here's a simple header file that defines a macro constant, variable constant, and a function.
#define MAX_VALUES 1024
const int NUM_PLANETS = 8; // Don't get Pluto started ...
int
fun (int x);
-
Put that code in the file
header.h
. -
Here's a library file that uses that header. Save it as
library.c
.#include "header.h" int fun (int x) { return x; // wasn't that fun? } // fun
-
Verify that you can compile
library.o
as follows:clang -c library.c
This command only compiles the source code present in the given file, leaving it as an object file. Because it has not been linked with the standard library, it is not a complete program executable. (We'll learn more about this process later in the course.) -
What do you expect to have happen if you inadvertantly include
header.h
twice? After answering, check your answer experimentally. -
Believe it or not, but multiple header inclusion can actually be a
problem. Why? Because many headers include other headers, so while you don't
think you're including the same header multiple times, you
actually are. How do we get around this problem? You've already seen the
typical strategy.
- We define some name the first time we load the header.
- The next time we load the header, we check if that name is defined.
-
We use a silly name like
__HEADER_H__
(read asunderscore, underscore, header name, underscore, capital H, underscore, underscore
).
header.h
, add
At the bottom of#ifndef __HEADER_H__ #define __HEADER_H__
header.h
, add#endif
-
Now, see what happens when you include
header.h
multiple times. You'll find it useful to look at the preprocessor output.clang -E library.c
Topic E: Why Use Macros
Given some of the costs we've seen associated with macros, why would you use them? Let's explore some reasons. Be prepared to discuss these in the next class.
Exercise 13: Testing
One of the best reasons to use Macros is that macros give you really fun capabilities, such as the ability to turn some code into a string. For example, here is a macro you might find useful.
#define TEST(EXP,RESULT) if (EXP != RESULT) { ++errors; printf ("Did not get expected result for %s.\n", #EXP); }
Check whether the macro works as you expect.
Exercise 14: Some Empirical Comparisons
Because C programmers often care about making the best use of resources, and there is a slight overhead involved in each function call. Let's explore that overhead.
-
Create a new program,
max3.c
, that includes your definitions ofmaxf
andMAXM
and that has the followingmain
procedure.int main (void) { int i; int x = 0; for (i = 0; i < 10000000; i++) x = maxf (x, i); return 0; } // main
- What does this code seem to do? Why might it be useful for examining the overhead used in a function call?
-
Compile your program and then run it with the following command, which tells
you how much computer time has been used.
time ./max3
-
Change the call from
maxf
toMAXM
, recompile, and retime the program. How much time was saved? Is that a lot or a little?
Exercise 15: A General Answer
We've written maxf
to find the maximum of two integers.
However, its name might lead someone to believe that it finds the maximum of
two floating point values.
-
What do you expect
maxf
to do when givenfloat
values as parameters. For example, what output do you expect for the following?float f = maxf (3.5f, 2.3f); printf ("The max is %f\n", f);
- Add the code to your program and check your answer experimentally.
-
What do you expect
MAXM
to do when givenfloat
values as parameters? - Change the code above to check your answer experimentally.
- Why might someone consider this an advantage of using macros?