ECE-1021 Lesson 2

Macros

(Last Mod: 27 November 2010 21:38:39 )

ECE-1021 Home



Objectives


Prerequisite Material


Co-requisite Material


A Slightly Leaner "Hello World!" Program

The following is a code listing for hello2.c, our next version of the Hello World! program:

#include <stdio.h>  // stdout, putc()

 

#define EXIT_PASS (0)

#define PutC(c) (putc((c), stdout))

 

int main(void)
{
    PutC('H');
    PutC('e');
    PutC('l');
    PutC('l');
    PutC('o');
    PutC(' ');
    PutC('W');
    PutC('o');
    PutC('r');
    PutC('l');
    PutC('d');
    PutC('!');

    PutC('\n');


    return EXIT_PASS;
}

The above program reflects the addition of just one element to our beginning collection of C tools, namely macros. Macros come in two varieties - object-like and function-like. This short program uses both. We will also examine an object-like macro from the Source Code Template.


Macros

The #define preprocessor directive creates a "macro".

The code above uses this directive to create an object-like macro in the following line:

#define EXIT_PASS (0)

The next line then defines a function-like macro:

#define PutC(c) (putc((c), stdout))

From a syntax standpoint, the difference is determined by whether the macro name is immediately followed by a left-parenthesis or not. If it is, then the compiler recognizes it as a function-like macro. If any white space at all follows the macro name, then the compiler interprets it at an object-like macro.

Object-like macros work almost like a global search and replace command in that wherever an occurrence of the macro name appears it gets replaces with the string of characters that follow the macro name in the #define statement.

But, unlike most editor's search and replace function, the preprocessor knows enough about the syntax of the C language to avoid replacing occurrences of the macro name that occur within comments, character constants, and string literals. By the same time, it can recognize and replace occurrences of the macro name within expressions even when there is no white space surrounding the name.

Example:

#define PI 3.14

double freq, w2PIf;

 

freq = 60.0;

printf("The value of PI is %f\n", PI); // Display PI

w2PIf = 2*PI*freq;

The letter sequence PI appears many times in the above code fragment.

  1. When the macro PI is defined.
  2. When the identifier w2PIf is declared. No macro expansion occurs here because the letters PI are only a part of the larger identifier name.
  3. Within the string literal of the call to printf(). Here again no expansion takes place because it is within a string literal.
  4. The second argument of the printf() call. Here it is recognized and replaced.
  5. In the comment following the printf() call. Here is it not replaced because it is in a comment.
  6. In the identifier w2PIf on the next line. Again, it is not expanded.
  7. In the expression 2*PI*freq on that same line. Here is it replaced even though it is embedded in a larger expression. The preprocessor breaks all lines and expressions into "tokens" which form the basic building blocks that the compiler will work with. That line consists of eight tokens, one of which is an occurrence of PI that gets replaced.

To understand the processing of that last line a little better, it helps to understand the notion of "tokens". The preprocessor breaks the entire source code file into a series of tokens, which are nothing more than the basic building blocks that will be passed on to the compiler for translation into the executable code. That last line is broken into the following eight tokens:

[w2PIf][=][2][*][PI][*][freq][;]

As you can see, one of those tokens if PI and that token gets expanded, so the token chain then becomes:

[w2PIf][=][2][*][3.14][*][freq][;]

Now, since the preprocessor knows how to evaluate constant expressions, what will probably get passed to the compiler is the following token chain:

[w2PIf][=][6.28][*][freq][;]

Naturally, this discussion is simplified and leaves quite a bit out, but hopefully the basic concept of macro creation and expansion is a little clearer.


Uses of Object-like Macros

The EXIT_PASS macro is used as a "symbolic constant" in that it has a constant value that is represented by a symbol (the name of the macro) that carries information for the humans reading the code. By defining this macro, we can use the name in the return statement at the end of the program and then, whenever we or anyone else examines the code, they see that we are returning a value called EXIT_PASS and, without even knowing what value it happens to have, can tell that the program has not failed. Elsewhere, we might have the following statement:

return EXIT_FAIL_DIVIDEbyZERO;

Hopefully its obvious that this is much more value laden than the statement:

return 13;

Even though they might well be one and the same as far as the compiler is concerned.

If we examine the code in the Source Code Template we find the following macro three macro definitions:

#define BLANKLINE (putc('\n', stdout))

#define MARGIN (2)                 /* Left Margin in PrintHeader() */
#define LMARG printc(' ', MARGIN)  /* Print Left Margin */

The last two of these macros are used in the function PrintHeader(), which is reproduced below:

void PrintHeader(void)
{
    LMARG; printc('=', (78 - MARGIN)); putc('\n', stdout);
    LMARG; printf("Course....... %s-%i (%s %i)\n",COURSE,SECTION,TERM,YEAR);
    LMARG; printf("Programmer... %s (%s)\n", PROGRAMMER, PROG_CODE);
    LMARG; printf("Assignment... %s (Rev %i)", ASSIGNMENT, REVISION);
    LMARG; printf("(Source Code in %s)\n", FILENAME);
    LMARG; printf("Description.. %s\n", TITLE);
    LMARG; printf(" %s\n", SUBTITLE);
    LMARG; printc('=', (78 - MARGIN)); putc('\n', stdout);
}

The LMARG macro causes MARGIN spaces to be printed to the screen. If we change the MARGIN definition to 5, then five spaces would be printed. As long as the active position on the screen is located at the beginning of a line and we invoke the LMARG macro before we print anything else out, the effect will be to effectively establish a left margin of MARGIN characters.

The first and last lines of the PrintHeader() function call the printc() function telling it to print out a series of equal signs to create a top and bottom border for the screen header. If we increase the number of spaces that we print at the beginning of the line, we must decrease the number of spaces that we print out otherwise we could cause the text to wrap to the next line. So we key the number of equal signs to the value of MARGIN with the result being that, unless MARGIN is 78 or more, the last equals sign will always appear in the same place on the screen.

Similarly, if you were writing your code with the goal of aligning it with the screen header, you could use the MARGIN macro in a similar fashion. If enough care and discipline is exercised in keying the code to macros, then it because possible to make significant changes to the behavior of a program with only a few simple changes to the code.

The LMARG macro doesn't serve much of a purpose - if anything it makes the code in PrintHeader() a bit less clear - at least at first. The only reason the macro was created was to reduce the length of the text on each line in the function body so that there would be a convenient one-to-one relationship between the text in the source code and the text printed to the screen. This is also why two statements are on each line of source code. Whether these benefits actually outweigh the cost associated with making the code less obvious and violating the one-statement-per-line convention are mattera for legitimate debate.

Notice that we could have used the BLANKLINE macro at the end of the lines that print out the border. The reason that we didn't is because the motivation for this macro is to use it where we want to actually see a blank line of text of the screen and that will only happen if we call this macro when the active position is at the beginning of a line (more specifically, a line which is empty). If our actual screen output has several blank lines separating it into sections and our code uses the BLANKLINE macro to create those divisions, then it makes correlating our code with the screen output much easier - but that will not be the case if we litter our code with BLANKLINE calls that are really just moving the active position to the next line and not creating a blank line in the process. The point here is that we can use macros to significantly improve the readability and maintainability of our code, but it is not a blind process - it still requires some discipline on our part.


Uses of Function-like Macros

Function-like macros are usually used as something of an intermediate step between an object-like macro, which has no flexibility whatever, and an actual function. It is frequently used when the function we would write would be extremely short, particularly if it were to end up being a single line of code in a return statement.

The function-like macro PutC() in the code fragment at the beginning of this lesson is primarily there to save us some typing and make the bulk of our code a bit cleaner in appearance.

But macros can be more flexible than functions in some ways. In particular, macros have no concept of the notion of data type - which can be good or bad. We can use it to our advantage in a macro such as:

#define min(a,b) ( ((a) < (b))? (a) : (b) )

If we were to write a function to perform this tasks, namely return the smaller of the two values passed to it, we would have to declare what data types each parameter was. Then, when we used it, we would be limited to those data types, either through implicit or explicit conversions. This macro can handle the various data types with far fewer problems since expression that we could use in the macro text on a instance-by-instance basis will work if those same expressions are passed as arguments to the macro.

Another example of a useful function-like macro is something to compare two floating point values to determine if they are close enough to being the same to be considered "equal" for a given purpose:

#define TOLERANCE (0.001)

#define tolequal(a, b, tol)  (   (tol)*(((a)/2.0)+((b)/2.0)) \

                               > ((a)>(b)?(a)-(b):(b)-(a))   )

#define fequal(a,b) (tolequal((a),(b), TOLERANCE))

The tolequal() macro looks for the values of a and b to be within a certain fraction of each other. Because the replacement string is so long, we have continued it onto a second line by ending the prior line with a backslash. The preprocessor will combine two physical source code lines into one logical source code line if the first one ends with a backslash immediately followed by the new line character (i.e., no additional white space is allowed).

The macro is written such that the programmer can select the tolerance independently for each call. In most cases, it will be sufficient to say that two floating point values are the same if they are within a fixed percentage of each other. So we create a symbolic constant called TOLERANCE and set it to 0.001 (0.1%) and then create a second function-like macro called fequal() that is keyed to that symbolic constant. If we only use the fequal() macro then we can set the required tolerance for all floating point comparisons by changing the value associated with the symbolic constant. We could even create additional macros with tighter and looser tolerances as follows:

#define TIGHT_TOLERANCE (0.001*TOLERANCE)

#define LOOSE_TOLERANCE (10.0*TOLERANCE)

#define l_equal(a,b) (tolequal((a),(b), LOOSE_TOLERANCE))

#define t_equal(a,b) (tolequal((a),(b), TIGHT_TOLERANCE))