Debugging with the Preprocessor in C Programming Language

As noted in Chapter 13, “The Preprocessor,” conditional compilation is useful when debugging programs. The C preprocessor can be used to insert debugging code into your program. By appropriate use of #ifdef statements, the debugging code can be enabled or disabled at your discretion. Program 18.1 is a program (admittedly contrived) that reads in three integers and prints out their sum. Note that when the preprocessor identifier DEBUG is defined, the debugging code (which prints to stderr) is compiled with the rest of the program, and when DEBUG isn’t defined, the debugging code is left out.

Program 18.1   Adding Debug  Statements  with the Preprocessor

#include <stdio.h>

#define DEBUG

int process (int i, int j, int k)

{

return i + j + k;

}

int main (void)

{

int i, j, k, nread;

nread = scanf (“%d %d %d”, &i, &j, &k);

#ifdef DEBUG

fprintf (stderr, “Number of integers read = %i\n”, nread);

fprintf (stderr, “i = %i, j = %i, k = %i\n”, i, j, k);

#endif

printf (“%i\n”, process (i, j, k));

return 0;

}

Program 18.1   Output

1 2 3

Number of integers read = 3

i = 1, j = 2, k = 3

6

Program 18.1   Output (Rerun)

1 2 e

Number of integers read = 2

i = 1, j = 2, k = 0

3

Note that the value displayed for k can be anything because its value was not set by the scanf call and it was not initialized by the program.

The statements

#ifdef DEBUG

fprintf (stderr, “Number of integers read = %i\n”, nread);

fprintf (stderr, “i = %d, j = %d, k = %d\n”, i, j, k);

#endif

are analyzed by the preprocessor. If the identifier DEBUG has been previously defined (#ifdef DEBUG), the preprocessor sends the statements that follow up to the #endif (the two fprintfs) to the compiler to be compiled. If DEBUG hasn’t been defined, the two

fprintfs never make it to the compiler (they’re removed from the program by the pre- processor). As you can see, the program prints out messages after it reads in the integers. The second time the program is run, an invalid character is entered (e). The debugging output informs you of the error. Note that to turn off the debugging code, all you have to do is remove the line

#define DEBUG

and the fprintfs are not compiled with the rest of the program. Although this program is so short you might not feel it’s worth the bother, consider how easy it is to turn debugging code on and off in a program several hundreds of lines long by simply chang- ing one line.

You can even control the debugging from the command line when the program is compiled. If you’re using gcc, the command

gcc –D DEBUG debug.c

compiles the file debug.c, defining the preprocessor variable DEBUG for you. This is equivalent to putting the following line in your program:

#define DEBUG

Take a look at a slightly longer program. Program 18.2 takes up to two command-line arguments. Each of these is converted into an integer value and is assigned to the corre- sponding variables arg1 and arg2. To convert the command-line arguments into inte- gers, the standard library function atoi is used. This function takes a character string as its argument and returns its corresponding representation as an integer. The atoi func- tion is declared in the header file <stdlib.h>, which is included at the beginning of Program 18.2.

After processing the arguments, the program calls the process function, passing the two command-line values  as arguments. This function simply returns the product of these two arguments. As you can see, when the DEBUG identifier is defined, various debugging messages are printed, and when it isn’t defined, only the result is printed.

Program 18.2   Compiling  in Debug  Code

#include <stdio.h>

#include <stdlib.h>

int process (int i1, int i2)

{

int val;

#ifdef DEBUG

fprintf (stderr, “process (%i, %i)\n”, i1, i2);

#endif

val = i1 * i2;

#ifdef DEBUG

fprintf (stderr, “return %i\n”, val);

#endif

return val;

}

int main (int argc, char *argv[])

{

int arg1 = 0, arg2 = 0;

if (argc > 1)

arg1 = atoi (argv[1]);

if (argc == 3)

arg2 = atoi (argv[2]);

#ifdef DEBUG

fprintf (stderr, “processed %i arguments\n”, argc – 1);

fprintf (stderr, “arg1 = %i, arg2 = %i\n”, arg1, arg2);

#endif

printf (“%i\n”, process (arg1, arg2));

return 0;

}

Program 18.2   Output

$ gcc –D DEBUG p18-2.c Compile with DEBUG defined

$ a.out 5 10

processed 2 arguments

arg1 = 5, arg2 = 10

process (5, 10)

return 50

50

Program 18.2   Output (Rerun)

$ gcc p18-2.c         Compile without DEBUG defined

$ a.out 2 5

10

When the program is ready for distribution, the debugging statements can be left in the source file without affecting the executable code, as long as DEBUG isn’t defined. If a bug is found at some later time, the debugging code can be compiled in and the output examined to see what’s happening.

The previous method is still rather clumsy because the programs themselves tend to be difficult to read. One thing you can do is change the way the preprocessor is used.

You can define a macro that can take a variable number of arguments to produce your debugging output:

#define DEBUG(fmt, …) fprintf (stderr, fmt, __VA_ARGS__)

and use it instead of fprintf as follows: DEBUG (“process (%i, %i)\n”, i1, i2); This gets evaluated as follows:

fprintf (stderr, “process (%i, %i)\n”, i1, i2);

The DEBUG macro can be used throughout a program, and the intent is quite clear, as shown in Program 18.3.

Program 18.3   Defining  a DEBUG Macro

#include <stdio.h>

#include <stdlib.h>

#define DEBUG(fmt, …) fprintf (stderr, fmt, __VA_ARGS__)

int process (int i1, int i2)

{

int val;

DEBUG (“process (%i, %i)\n”, i1, i2);

val = i1 * i2;

DEBUG (“return %i\n”, val);

return val;

}

int main (int argc, char *argv[])

{

int arg1 = 0, arg2 = 0;

if (argc > 1)

arg1 = atoi (argv[1]);

if (argc == 3)

arg2 = atoi (argv[2]);

DEBUG (“processed %i arguments\n”, argc – 1);

DEBUG (“arg1 = %i, arg2 = %i\n”, arg1, arg2);

printf (“%d\n”, process (arg1, arg2));

return 0;

}

Program 18.3   Output

$ gcc pre3.c

$ a.out 8 12

processed 2 arguments

arg1 = 8, arg2 = 12

process (8, 12)

return 96

96

As you can see, the program is much more readable in this form. When you no longer need debugging output, simply define the macro to be nothing:

#define DEBUG(fmt, …)

This tells the preprocessor to replace calls to the DEBUG macro with nothing, so all uses of DEBUG simply turn into null statements.

You can expand on the notion of the DEBUG macro a little further to allow for both compile-time and execution-time debugging control: Declare a global variable Debug that defines a debugging level. All DEBUG statements  less than or equal to this level pro- duce output. DEBUG now takes at least two arguments; the first is the level:

DEBUG (1, “processed data\n”);

DEBUG (3, “number of elements = %i\n”, nelems)

If the debugging level is set to 1 or 2, only the first DEBUG statement produces output; if the debugging level is set to 3 or more, both DEBUG statements produce output. The debugging level can be set via a command-line option at execution time as follows:

a.out –d1           Set debugging level to 1

a.out -d3           Set debugging level to 3

The definition for DEBUG is straightforward:

#define DEBUG(level, fmt, …) \

if (Debug >= level) \

fprintf (stderr, fmt, __VA_ARGS__)

So

DEBUG (3, “number of elements = %i\n”, nelems);

becomes

if (Debug >= 3)

fprintf (stderr, “number of elements = %i\n”, nelems);

Again, if DEBUG is defined to be nothing, the DEBUG calls become null statements.

The following definition provides all the mentioned features, as well as the ability to control the definition of DEBUG at compile time.

#ifdef DEBON

# define DEBUG(level, fmt, …) \

if (Debug >= level) \

fprintf (stderr, fmt, VA_ARGS )

#else

# define DEBUG(level, fmt, …)

#endif

When compiling a program containing the previous definition (which you can conve- niently place inside a header file and include in your program), you either define DEBON or not. If you compile prog.c as follows:

$ gcc prog.c

it compiles in the null definition for DEBUG based on the #else clause shown in the pre- vious preprocessor statements. On the other hand, if you compile your program like this:

$ gcc –D DEBON prog.c

the DEBUG macro that calls fprintf based on the debug level is compiled in with the rest of your code.

At runtime, if you have compiled in the debugging code, you can select the debug level. As noted, this can be done with a command-line option as follows:

$ a.out –d3

Here, the debug level is set to 3. Presumably, you would process this command-line argu- ment in your program and store the debug level in a variable (probably global) called Debug. And in this case, only DEBUG macros that specify a level of 3 or greater cause the fprintf calls to be made.

Note that a.out -d0 sets the debugging level to zero and no debugging output is generated even though the debugging code is still in there.

To summarize, you have seen here a two-tiered debugging scheme: Debugging code can be compiled in or out of the code, and when compiled in, different debugging levels can be set to produce varying amounts of debugging output.

Source: Kochan Stephen G. (2004), Programming in C: A Complete Introduction to the C Programming Language, Sams; Subsequent edition.

Leave a Reply

Your email address will not be published. Required fields are marked *