CSC161 2011S Imperative Problem Solving

Laboratory: The C Preprocessor and Macros

Summary:

Prerequisites: Familiarity with basic C. Familiarity with Linux.

Preparation

a. Create a directory for this lab. I would recommend csc161/labs/preprocessor.

b. In that directory, create the following C program and call it sample1.c.

#define SIZE 5

int
main ()
{
  int i;
  int a[SIZE];

  a[0] = 0;
  for (i = 1; i < SIZE; i++)
    a[i] = 2*i + a[i-1];

  return 0;
} // main

Exercises

Topic A: Naming Constants

Exercise 1: Simple Definitions

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

cc -E sample1.c

b. 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]);

c. What do you expect to have happen if you preprocess the new file?

d. Check your answer experimentally.

e. What do you expect to have happen if you compile and run the program?

f. Check your answer experimentally.

g. Replace the line that defines SIZE with

#define SIZE 3+4

h. What do you expect to happen if you preprocess the revised file?

i. Check your answer experimentally.

j. What do you expect to have happen if you compile and run the program?

k. Check your answer experimentally.

l. Restore the #define with

#define SIZE 5

Exercise 2: Basics of #include

As you know, the C compiler really likes to have functions predeclared. As you also know, those predeclarations often appear in header files, files with a suffix of .h. In fact, you've probably just seen some warning messages because you failed to include <stdio.h>.

a. Add the #include line to your program.

b. What do you expect to have happen if you now compile and run your program?

c. Check your answer experimentally.

d. What do you expect to see when you preprocess the file?

e. Check your answer experimentally.

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. Let's try adding one.

a. Replace the #define in sample1.c with

#define SIZE 5;

b. What do you expect to have happen if you now compile and run your program?

c. Check your answer experimentally.

d. What do you expect to see when you preprocess the file?

e. Check your answer experimentally.

Exercise 4: More Ways to Define Constants

Let's explore other ways to think about associating values with names.

a. Remove the #define line from sample1.c.

b. What do you expect to see when you preprocess the file?

c. Check your answer experimentally.

d. What do you expect to have happen if you now compile and run your program?

e. Check your answer experimentally.

f. It turns out that you can also define names on the command line with -DNAME=VALUE. Let's try setting SIZE to 10 with

cc -DSIZE=10 -o sample1 sample1.c

g. Run the newly compiled version of sample1 to see if we get the expected result.

h. Discuss with a neighbor 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

a. Reinsert the following line at the top of sample1.c.

#define SIZE 5

b. What do you expect to have happen when you try to compile the program using the following command?

cc -DSIZE=10 -o sample1 sample1.c

c. Check your answer experimentally.

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

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

As you no doubt have noticed, 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.

Consider the following simple program, which counts the number of command-line parameters that start with a period. E.g., if we type

./countdots hello .goodbye zebra .stripes thats-all-folks

we should get an output of 2, for the .goodbye and .stripes.

#include <stdio.h>

int
main ()
{
  int i;        // Index into argv
  int dots;     // The number of strings that start with a dot.

  for (i = 0; i < argc; i++)
    {
      if (argv[++i][0] == '.')
        {
          ++dots;
        } // if it starts with a dot
    } // for

  printf ("%d\n", dots);
} // main

Unfortunately, it's not quite right. For example, we sometimes don't seem to get the right output foar the following:

./countdots hello .goodbye .zebra .stripes and-thats-it
./countdots hello .goodbye zebra .stripes

a. Try the examples (and a few others) to make sure.

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 the top of the loop.

      printf ("i = %d, argv[i] = %s, dots = %d.\n", i, argv[i], dots);

b. Add the line and rerun the examples. Have you spotted the problems?

c. 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, argv[i] = %s, dots = %d.\n", i, argv[i], dots);
#endif // TESTING

d. Compile the program (without setting the TESTING flag).

e. Now, suppose that we realize that there's another error. We want to test again. Compile the program, setting the testing flag, as in

$ gcc -Wall -DTESTING -o countdots countdots.c

f. Verify that the testing messages now reappear.

Topic C: Macros

Exercise 7: Maximal Macros

As you may recall, the definition of the max function is fairly simple. (We've 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

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

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

#define MAXM(X,Y) X > Y ? X : Y

Add this macro to max1.c.

c. Replace the call to maxf with a call to MAXM.

d. What do you expect to see when you run the preprocessor on max1.c?

e. Check your answer experimentally.

f. Do your expect the results from the revised max1.c to differ? If so, how?

g. Check your answer experimentally.

Exercise 8: A Squared Detour

K&R caution us about a danger implicit in the following definition.

#define SQUARE(X) X*X

Let's explore that danger.

a. Create a program, sample2.c, that uses this macro to compute the square of 5. Print the result. Do you get the output you expect?

b. Modify your program so that it instead computes the square of 2+3. Do you still get the result you expect?

c. 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?

cc -E sample2.c

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

int errors = 0;

void
check3 (int a, int b, int c, int expected)
{
  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);
      ++errors;
    }
} // max3

int
main ()
{
  // Check various permutations
  check3 (1, 2, 3, 3);
  check3 (1, 3, 2, 3);
  check3 (2, 1, 3, 3);
  check3 (2, 3, 1, 3);
  check3 (3, 1, 2, 3);
  check3 (3, 2, 1, 3);

  return errors;
} // main

a. Determine whether or not maxf successfully meets these additional tests.

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

c. As K&R implicitly suggest, 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?

a. What do you expect to have happen if you add a semicolon to the definition of MAXM given above?

b. Check your asnwer experimentally.

Exercise 11: Even More Fun with Macros

K&R also raise an issue with their version of MAX. Let's explore their concern.

a. 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);

b. Check your answer experimentally.

c. We would expect MAXM to give the same results. Determine experimentall whether or not it does.

d. Be prepared to summarize what went wrong in this case.

e. 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 constant and a function.

#define MAX_VALUES 1024

int fun (int x);

a. Put that code in the file header.h.

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

c. Verify that you can make library.o.

d. What do you expect to have happen if you inadvertantly include header.h twice?

e. Check your answer experimentally.

f. Believe it or not, but multiple header inclusion is a real 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.

Let's try. At the top of header.h, add

#ifndef __HEADER_H__
#define __HEADER_H__

At the bottom of header.h, add

#endif

g. Now, see what happens when you include header.h multiple times. (You'll find it useful to look using the preprocessor.)

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.

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.

a. Create a new program, max3.c, that includes your definitions of maxf and MAXM and that has the following main procedure.

int
main ()
  int i;
  int x = 0;

  for (i = 0; i < 10000000; i++)
    x = maxf (x, i);

  return 0;
} // main

b. What does this code seem to do? Why might it be useful for examining the overhead used in a function call?

c. Compile your program and then run it with the following command, which tells you how much computer time has been used.

time ./max3

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

a. 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.5, 2.3);
  printf ("The max is %f\n", f);

b. Check your answer experimentally.

c. What do you do MAXM to do when given float values as parameters?

d. Check your answer experimentally.

e. Why might someone consider this an advantage of using macros?

 

History

Thursday, 14 October 2010 [Samuel A. Rebelsky]

  • Started writing

Friday, 15 October 2010 [Samuel A. Rebelsky]

Friday, 11 March 2011 [Samuel A. Rebelsky]

 

Disclaimer: I usually create these pages on the fly, which means that I rarely proofread them and they may contain bad grammar and incorrect details. It also means that I tend to update them regularly (see the history for more details). Feel free to contact me with any suggestions for changes.

This document was generated by Siteweaver on Fri Mar 11 09:21:06 2011.
The source to the document was last modified on Fri Mar 11 09:21:03 2011.
This document may be found at http://www.cs.grinnell.edu/~rebelsky/Courses/CSC161/2011S/Labs/preproc-lab.html.
A PDF version of this document may be found at http://www.cs.grinnell.edu/~rebelsky/Courses/CSC161/2011S/Labs/preproc-lab.pdf

Samuel A. Rebelsky, rebelsky@grinnell.edu

Copyright © 2010 Samuel A. Rebelsky. This work is licensed under a Creative Commons Attribution-NonCommercial 2.5 License. To view a copy of this license, visit or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.