CSC 161 Schedule Readings Labs & Projects Homework Deadlines Resources

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

Exercise 1: Simple Definitions and Basics of #include

  1. Copy the C program preprocess.c to your labs 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
    
  2. 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.
  3. 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 the return statement.
      for (i = 0; i < SIZE; i++)
        printf ("a[%d] = %d\n", i, a[i]);
  4. What do you expect to have happen if you preprocess the new file? After answering, check your answer experimentally.
  5. What do you expect to have happen if you compile and run the program? After answering, check your answer experimentally.
  6. Add the necessary #include directive to your program.
  7. What do you expect to happen if you preprocess the revised file? After answering, check your answer experimentally.
  8. What do you expect to have happen if you compile and run the program? After answering, check your answer experimentally.
  9. Replace the line that defines SIZE with
    #define SIZE 3+4
  10. What do you expect to happen if you preprocess the revised file? After answering, check your answer experimentally.
  11. What do you expect to have happen if you compile and run the program? After answering, check your answer experimentally.
  12. 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.

  1. Replace the #define in your program preprocess.c with
    #define SIZE 5;
  2. What do you expect to have happen if you now compile and run your program? After answering, check your answer experimentally.
  3. 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.

  1. Remove the #define line from your program preprocess.c.
  2. What do you expect to see when you preprocess the file? After answering, check your answer experimentally.
  3. What do you expect to have happen if you now compile and run your program? After answering, check your answer experimentally.
  4. You can also define names on the compile command line with -DNAME=VALUE. Let's try setting SIZE to 10 with the following command.
    clang -DSIZE=10 -o preprocess preprocess.c
  5. Run the newly compiled version of preprocess to see if we get the expected result.
  6. 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

  1. Reinsert the following line at the top of your program.
    #define SIZE 5
  2. 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.
  3. 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
  4. 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.

  1. Add the following declaration
    int numEvens = 0;
    to your program and add the following line to the end of the first for loop
      if (a[i] %2 == 0)
        numEvens++;
    Finally, print the value of numEvens before the return, like thus.
      printf ("Number of even numbers: %d\n", numEvens);
  2. What do you expect the value of numEvens to be when you compile (leaving SIZE at 5)? After answering, check your answer experimentally.
  3. 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);
  4. Now rerun the example. Have you spotted the problem?
  5. Correct the problems you've observed.
  6. 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
  7. Compile the program (without setting the TESTING flag). Verify it runs without the test output.
  8. 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
    or
    make CPPFLAGS=-DTESTING preprocess
  9. 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
  1. 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 for maxf and your test, put them all in one file anyway. Call your program max1.c.
  2. 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.
    #define MAXM(X,Y) X > Y ? X : Y
    Add this macro to max1.c.
  3. Replace the call to maxf with a call to MAXM.
  4. What do you expect to see when you run the preprocessor on max1.c? After answering, check your answer experimentally.
  5. 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.

  1. 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?
  2. Modify your program so that it instead computes the square of 2+3. That is, use
    SQUARE( 2+3 )
    Do you still get the result you expect?
  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
  4. 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
  1. Determine whether or not maxf successfully meets these additional tests.
  2. Determine whether or not MAXM successfully meets these additional tests. (Note that you'll need to replace each instance of maxf in check3 with MAXM).
  3. 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.
    #define MAXM(X,Y) ((X) > (Y) ? (X) : (Y))
    Verify that this definition passes the tests.

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.

  1. What value do you expect a, b, and c to have in the following expression? Recall that ++var adds one to var and gives back the new value of var.
      int a = 5;
      int b = 7;
      int c = maxf (++a, ++b);
  2. Check your answer experimentally.
  3. We perhaps might expect MAXM to give the same results. Determine experimentally whether or not it does.
  4. Explain went wrong in this case.
  5. 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);
  1. Put that code in the file header.h.
  2. 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
  3. 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.)
  4. What do you expect to have happen if you inadvertantly include header.h twice? After answering, check your answer experimentally.
  5. 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 as underscore, underscore, header name, underscore, capital H, underscore, underscore).
    Let's try. At the top of header.h, add
    #ifndef __HEADER_H__
    #define __HEADER_H__
    At the bottom of header.h, add
    #endif
  6. 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.

  1. Create a new program, max3.c, that includes your definitions of maxf and MAXM and that has the following main procedure.
    int
    main (void) {
      int i;
      int x = 0;
    
      for (i = 0; i < 10000000; i++)
        x = maxf (x, i);
    
      return 0;
    } // main
  2. What does this code seem to do? Why might it be useful for examining the overhead used in a function call?
  3. Compile your program and then run it with the following command, which tells you how much computer time has been used.
    time ./max3
  4. Change the call from maxf to MAXM, 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.

  1. What do you expect maxf to do when given float 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);
  2. Add the code to your program and check your answer experimentally.
  3. What do you expect MAXM to do when given float values as parameters?
  4. Change the code above to check your answer experimentally.
  5. Why might someone consider this an advantage of using macros?